Taula de continguts

NodeJS amb Express

NodeJS és una plataforma per a desenvolupament web amb JavaScript. Ha arribat a disposar de moltes llibreries que gestionem amb la utilitat NPM.

Express és un framework per a NodeJS que ens facilita la creació d'aplicacions web.

Referències:

, , , , , , , , , , , ,

Primeres passes

  1. Descarrega i instal·la NodeJS per a la teva plataforma. Convé descarregar la darrera versió LTS si volem després utilitzar algunes eines com Cordova (per a compilar sobre dispositius mòbils). La que hi ha als respositoris d'Ubuntu (nodejs-legacy) NO sol ser la darrera LTS. Millor descarregar-la de la pàgina oficial https://nodejs.org
  2. Crea una carpeta per a la teva nova app:
    $ mkdir myApp
  3. Entra a la carpeta:
    $ cd myApp
  4. Inicia un arxiu de projecte package.json amb:
    $ npm init
  5. Instal·la el framework Express amb
    $ npm install express
  6. Segueix l'exemple "Hello World!" de la documentació oficial de Express i implementa'l dins la nova carpeta.
  7. Arrenquem la app amb:
    $ node app.js
  8. Si tot ha anat bé, podràs visualitzar la teva app apuntant el navegador a:
    http://localhost:3000


Fer anar l'aplicació

Necessitem dues maneres de fer anar l'aplicació

Per la primera farem servir el paquet nodemon, i per la segona el paquet forever.

$ npm install nodemon
$ npm install forever

El paquet nodemon observa l'estructura de directoris i reinicia el servidor si hi ha canvis, així podem desenvolupar sense la molèstia d'anar reiniciant nosaltres el servidor

El paquet forever observa el servidor i el reinicia si veu que s'ha aturat o quedat penjat, així sabem que funciona encara que no estiguem vigilant contínuament

Hem de configurar l'arxiu package.json per poder arrencar el servidor en mode desenvolupament o en mode d'aplicació. Podem fer-ho amb un editor de text afegint scripts:

  "scripts": {
    "development": "nodemon app.js",
    "app": "forever app.js"
  }

L'arxiu package.json ha de ser semblant a aquest:

{
  "name": "myApp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.1",
    "forever": "^4.0.3",
    "nodemon": "^2.0.19"
  },
  "scripts": {
    "development": "nodemon app.js",
    "app": "forever app.js"
  }
}

Per arrencar l'aplicació quan estem desenvolupant:

$ npm run development

Per arrencar l'aplicació quan funciona normalment:

$ npm run app


El servidor més simple

Com que desenvolupem, arrencar el servidor amb npm run development, aleshores editar l'arxiu app.js per tal que quedi així:

    const express = require('express')
    const app = express()
    const port = 3000
 
    app.get('/', getHello)
    async function getHello (req, res) {
        res.send(`Hello World`)
    }
 
    app.listen(port, () => {
     console.log(`Example app listening on port ${port}`)
    })

Ja pots navegar a: http://localhost:3000 i comprovar que el servidor funciona

Les claus d'aquest codi són:


Configurant les URLs amb Express

Per afegir direccions URL amb express cal afegir entrades '.get' com en aquest exemple:

    const express = require('express')
    const app = express()
    const port = 3000
 
    app.get('/clients', getClients)
    async function getClients (req, res) {
        res.send(`Llista de clients`)
    }
 
    app.get('/login', getLogin)
    async function getLogin (req, res) {
        res.send(`Entra al sistema`)
    }
 
    app.get('/', getHello)
    async function getHello (req, res) {
        res.send(`Hello World`)
    }
 
    app.listen(port, () => {
     console.log(`Example app listening on port ${port}`)
    })

Fixa't que cada entrada de '.get' configura una direcció, i que es segueix la preferència segons s'han definit al codi. Ara ja es pot navegar a les diferents web:


Configurant una carpeta d'arxiu estàtics

Habitualment, els servidors tenen una carpeta de la que es serveixen els arxius tal i com estàn al disc, sense modificacions. D'aquests arxius en diem arxius estàtics. I normalment aquesta carpeta es diu public perquè quedi clar que el què conté està publicat a través de la xarxa.

Crea una carpeta anomenada public i posa-hi dins una foto anomenada foto.jpg

Aleshores afegeix aquesta línia de configuració al codi:

  app.use(express.static('public'))

Per tal que quedi semblant a aquest codi:

    const express = require('express')
    const app = express()
    const port = 3000
 
    app.use(express.static('public'))
 
    app.get('/clients', getClients)
    async function getClients (req, res) {
        res.send(`Llista de clients`)
    }
 
    app.get('/login', getLogin)
    async function getLogin (req, res) {
        res.send(`Entra al sistema`)
    }
 
    app.get('/', getHello)
    async function getHello (req, res) {
        res.send(`Hello World`)
    }
 
    app.listen(port, () => {
     console.log(`Example app listening on port ${port}`)
    })
 

Ara pots accedir als arxius de la carpeta public posant-ne la direcció com si fós una URL:

http://localhost:3000/foto.jpg

Fixa't que la carpeta public té preferència perquè està configurada la primera, però que les altres direccions continuen funcionant


Agafant paràmetres de la URL (Objecte 'req')

Les URLs poden contenir paràmetres, i aquests fan que es rebin uns continguts o uns altres. Els paràmetres van al final de la URL darrera d'un símbol de ? com en aquest exemple:

http://localhost:3000/llistat?cerca=cotxe

L'objecte req ens dóna informació de la crida que ens fa el client, i part d'aquesta informació són els paràmetres de la URL.

Per veure els paràmetres de la URL i els seus valors, des del codi de Node ho podem fer així:

  const express = require('express')
  const url = require('url')
  const app = express()
  const port = 3000
 
  app.get('/llistat', (req, res) => {
   let query = url.parse(req.url, true).query;
    if (query.cerca && query.color) {
     res.send(`Aquí tens el llistat de ${query.cerca} de color ${query.color}`)
    } else {
      res.send('Paràmetres incorrectes')
    }
  })
 
  app.listen(port, () => {
   console.log(`Example app listening on port ${port}`)
  })

Comprova la diferència al navegar a aquestes direccions:


Utilitzant plantilles EJS

EJS és un sistema de plantilles que ens facilita renderitzar pàgines web. En combinació amb NodeJS i Express ens resulta molt pràctic per separar el codi (processament) del HTML (presentació).

  1. Instal·la segons s'indica en la doc oficial de EJS
  2. Implementa una plantilla en un arxiu .ejs a part. Pots mirar aquest exemple de EJS amb Express
  3. Pensa que els arxius EJS per defecte van dins una carpeta views.
  4. Implementa una capçalera i peu de pàgina separats en arxius EJS i crida'ls amb un include.

L'objectiu ara serà que la nostra aplicació respongui a les dades que li enviem. Per això necessitem formularis…

La documentació de EJS és una mica escassa d'exemples. Si vols saber si una variable existeix o no dins d'una plantilla EJS NO testejis amb

<% if( var1 ) {...} %

sinó amb

<% if( locals.var1 ) {...} %>

tal i com expliquen en aquest post.


Formularis, GET i POST

Quan s'envia un formulari HTML les dades es poden adjuntar utilitzant diversos mètodes, els principals són GET i POST (però també hi ha PUT o DELETE).

Variables via mètode GET

Les dades enviades per GET podem accedir-les amb la variable req.query.

Per exemple, si tenim un formulari amb un

<form action="/checklogin">
    <input "name"="username">
    ...

les dades s'enviaran a la pàgina checklogin (action) dins la URL aproximadament així:

http://elmeudomini.com/checklogin?username=manolo

i podrem accedir via req.query.username.

No cal instal·lar cap complement.

Exercici

Crea una aplicació «Hello, Manolo» que demani el nom d'usuari en un formulari i que a l'enviar-ho via GET ens saludi amb «Hello, <nomusuari>».


Dades POST

Per les dades POST necessitem el package ''body-parser''.

Les dades enviades per POST es poden accedir amb la variable req.body.

Instal·la body-parser amb

npm install body-parser

Tens la documentació oficial del body-parser.

El codi d'exemple ens podria quedar així:

const express = require('express')
const app = express()
const port = 3000
 
// setup
app.set('view engine', 'ejs');
var bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
 
 
// routes
app.get('/', (req, res) => {
  res.render("form.ejs"); // cal crear una plantilla a views/form.ejs
})
app.post('/', (req, res) => {
  res.send("Welcome "+req.body.username);
});
 
 
// start app
app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})


Exercicis

  1. Seguint l'exemple anterior, fes una aplicació «Hello, Pepa» que demani el nom d'usuari en un formulari i que a l'enviar-ho ens saludi amb «Hello, <nomusuari>».
    • Si no s'ha introduit cap nom, posarà «Benvningut, Anònim».
    • Afegeix un radiobutton al formulari per indicar si el tractament ha de ser «Benvingut» o bé «Benvinguda».
  2. Realitza una aplicació de login d'usuaris amb NodeJS + Express.
    • Ha d'oferir un formulari que permeti introduir usuari i contrasenya.
    • Definirem els usuaris i contrasenyes en un JS Object (array associatiu).
    • Si l'usuari no existeix, mostrarem un missatge «usuari no existent».
    • Si l'usuari existeix, però la contrasenya és errònia, ens ha de mostrar «contrasenya incorrecta».
  3. Utilitza les plantilles EJS per fer les vistes del formulari i del resultat de la consulta del login.
    • Crea una plantilla base amb un header i footer personalitzat.
    • Aplica la plantilla base a les diferents views derivades.


Persistència amb localStorage

La persistència amb NodeJS, com és d'esperar, es pot fer mitjançant arxius i bases de dades. Hi ha una manera força fàcil d'implementar una persistència senzilla: localStorage. Aquest mecanisme és propi dels browsers, raó per la qual en NodeJS no està disponible de forma nadiua i caldrà carregar la llibreria node-localstorage.

localStorage és un sistema de persistència de tipus key-value, cosa que simplifica molt la forma de treballar, tot i que limita les seves features.

Per instal·lar-ho fes

$ npm install node-localstorage

En el fons aquesta llibreria node-localstorage utilitza arxius (podràs veure les variables emmagatzemades a la carpeta scratch si segueixes les instruccions de la documentació).

Si vols injectar dades al localStorage de la teva aplicació pots fer crear arxius a aquesta carpeta, o bé, de forma més «pulida», des de la consola:

$ node
> var LocalStorage = require('node-localstorage').LocalStorage;
> localStorage = new LocalStorage('./scratch');
> localStorage.setItem("clau","valor");

Objectes a localStorage

localStorage només emmagatzema strings. Si volem posar objectes complexes, com diccionaris o arrays, caldrà utilitzar algun mètode de serialització. Les funcions típiques que s'empren per a això son:

  1. JSON.stringify() ens transforma l'objecte en un string per poder ser insertat al localStorage.
  2. JSON.parse() ens permet recuperar un string serialitzat i el restitueix com a objecte.


Servint arxius estàtics

En principi, NodeJS no hauria de servir arxius estàtics (com imatges, scripts JS de client, etc.), ja que no és molt eficient realitzant aquesta tasca i és millor deixar-la per al servidor web, sigui Apache o Nginx o el què prefereixis.

Tot i això, en alguns casos volem facilitar-nos aquesta funcionalitat fent-ho des de Node i en particular amb Express. Podeu veure com servir arxius estàtics en aquest enllaç.


Càrrega d'arxius

Per carregar arxius podeu utilitzar el mòdul formidable de NPM.

Aquí teniu un breu però clar tutorial de formidable, i també podeu mirar la pàgina oficial de formidable a NPM.


Exercicis

  1. Crea una aplicació de tasques o tasklist utilitzant el localStorage com a sistema de persistència, tal i com s'explica més amunt.
  2. Amplia l'aplicació de login i/o la de tasklist , creant una taula d'usuaris en una base de dades (recomanem PostgreSQL si vols després utilitzar Heroku gratuïtament)
    • Fes que la columna de la contrasenya estigui encriptada amb SHA512.


APIs i JSON

Més endavant voldrem comprovar des d'una app mòbil si el login i pass és correcte. I per això no volem fer servir un formulari, sinó que volem poder fer una crida a una URL que ens pugui respondre OK o KO i prou, sense HTML.

De fet, la majoria d'APIs funcionen amb format JSON tant per a l'entrada com per a la sortida de dades. Per retornar dades en format JSON, que son els propis de JavaScript, només cal utilitzar res.json(jsonObj) , per exemple:

app.get('/api/get_user', (req, res) => {
  res.json( {"username":"manolo"} );
})

Aquesta crida remota és el que acostumem a denominar una «web» API («web» ja que la fem via HTTP).

Per testejar des de la shell fem les crides amb la comanda curl. En HTTP podem triar entre fer-les per GET o per POST i en diferents formats:

Teniu aquí més exemples de com es fan les crides amb curl.

Express ens permet recollir els paràmetres de la URL de forma molt fàcil amb la variable req.params.

app.get('/api/user/:id', function (req, res) {
  userId = req.params.id;
  userObj = getUser(userId);
  res.json()
})

Recopilant el dit fins ara, per accedir als paràmtres d'entrada de l'usuari amb Express utilitzarem:

Implementa la ruta /api/logincheck/, i respon si l'usuari i contrasenya enviats per GET són correctes o incorrectes.


Habilitant CORS

Les aplicacions web que correm en browsers amb JS solen tenir una protecció de seguretat per evitar crides entre diferents orígens de dades. És el que anomenem CORS o Cross-Origin Resource Sharing.

Pots mirar una demo de com funciona CORS a http://ieti.cat/cors/

Utilitza aquest mòdul per habilitar CORS a la nostra flamant API.

Mirant més de prop, podem comprovar si la nostra aplicació està funcionant amb CORS si trobem al seu headers el següent:

Access-Control-Allow-Origin: *

Per veure els headers de la response només cal que feu la crida curl -i


Autenticació a les APIs

Els mètodes més habituals d'autenticació per a APIs son:

Basic Auth

BasicAuth és un sistema molt simple i que no es recomana en entorns de producció. Tot i així, sol utilitzar-se en alguns entorns de test com a mètode simple d'autenticació. Per a NodeJS+express disposem del plugin express-basic-auth, senzill però efectiu per a aquesta tasca.

Per cridar una BasicAuth amb curl fem:

curl -u usuari:contrasenya http://...

Token Authentication i JWT

Podeu llegir sobre token authentication i les seves bones pràctiques. L'article explica força bé un tipus particular de token auth per al context web anomenat JWT (JSON Web Tokens) que utilitzen un sistema de signatura digital. La única pega que té és que té una certa complicació en la implementació.

Com a referència, aquí tens un tutorial per a JWT amb NodeJS, tingues en compte que utilitza MongoDB (amb mongoose per emmagatzemar els tokens. Utilitza'l només si has entès bé com emprar la signatura digital.


Posant NodeJS en producció amb Heroku

Segueix el tutorial NodeJS Heroku d'aquesta mateixa wiki.


Utilitzant MongoDB amb NodeJS

Segueix el tutorial accedint mongodb en què explica com utilitzar MongoDB amb PHP i NodeJS.


Accions en temps real amb Socket.IO

El protocol HTTP no és adequat per a temps real ja que no està orientat a connexió, i crea i destrueix el socket en cada request efectuada al servidor. Això es fa per tal d'alliberar recursos del servidor i no saturar-lo. La contrapartida és que afegeix un overhead de precessament que fa que per demanar dades molt sovint no sigui adequat. Per enviar dades repetidament (com podria ser el cas d'un chat, o millor encara, el moviment d'un joystick) sigui millor mantenir el socket obert.

Una eina adequada per acomplir això és Socket.IO . Recomanable el tutorial inicial per crear un programa de xat que il·lustra molt bé com treballar.