====== 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.
{{ nodejs-express.png?300 }}
Referències:
* [[NodeJS Heroku]] per posada en producció amb PaaS.
* [[Accedint MongoDB]] utilitzant NodeJS o PHP.
{{tag> #FpInfor #Daw #Dam #DawMp07 #DamMp06 #DamMp06Uf4 #DamMp06Uf04 #DawMpDual #DamMpDual nodejs node express webserver }}
===== Primeres passes =====
- 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
- Crea una carpeta per a la teva nova app:$ mkdir myApp
- Entra a la carpeta: $ cd myApp
- Inicia un arxiu de projecte ''package.json'' amb:$ npm init
- Instal·la el //framework// ''Express'' amb $ npm install express
- Segueix l'[[https://expressjs.com/es/starter/hello-world.html|exemple "Hello World!" de la documentació oficial de Express]] i implementa'l dins la nova carpeta.
- Arrenquem la app amb: $ node app.js
- 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ó
* Mentre desenvolupem, necessitem fer reinicis continuament per veure els nous canvis
* Mentre l'aplicació està en funcionament, s'ha de reiniciar sola si es penja o s'atura
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|http://localhost:3000]] i comprovar que el servidor funciona
Les claus d'aquest codi són:
* Fa servir 'express' per iniciar el servidor
* Escolta les peticions a través del port 3000
* Qualsevol URL retorna el text 'Hello World!'
* Al iniciar-se mostra el text "Example app ... 3000" a través del terminal
\\
===== 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:
* [[http://localhost:3000|http://localhost:3000]]
* [[http://localhost:3000/clients|http://localhost:3000/clients]]
* [[http://localhost:3000/login|http://localhost:3000/login]]
\\
===== 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|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|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:
* [[http://localhost:3000/llistat?cerca=cotxes&&color=blau|http://localhost:3000/llistat?cerca=cotxes&&color=blau]]
* [[http://localhost:3000/llistat?cerca=radio&&color=verd|http://localhost:3000/llistat?cerca=readio&&color=verd]]
\\
===== 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ó).
- Instal·la segons s'indica en la [[https://ejs.co/#install|doc oficial de EJS]]
- Implementa una plantilla en un arxiu .ejs a part. Pots mirar [[https://github.com/mde/ejs/wiki/Using-EJS-with-Express|aquest exemple de EJS amb Express]]
- Pensa que els arxius EJS per defecte van dins una carpeta ''views''.
- 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 [[https://stackoverflow.com/questions/5372559/what-is-the-proper-way-to-check-for-existence-of-variable-in-an-ejs-template-us|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
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, ».
\\
==== Dades POST ====
Per les dades ''POST'' necessitem el [[https://www.npmjs.com/package/body-parser|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 [[https://www.npmjs.com/package/body-parser|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 ====
- 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, ".
* 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".
- 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".
- 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 [[https://www.npmjs.com/package/node-localstorage|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:
- [[https://www.w3schools.com/js/js_json_stringify.asp|JSON.stringify()]] ens transforma l'objecte en un string per poder ser insertat al ''localStorage''.
- [[https://www.w3schools.com/js/js_json_parse.asp|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. [[https://expressjs.com/es/starter/static-files.html|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.
[[https://www.w3schools.com/nodejs/nodejs_uploadfiles.asp|Aquí teniu un breu però clar tutorial de formidable]], i també podeu mirar la [[https://www.npmjs.com/package/formidable|pàgina oficial de formidable a NPM]].
\\
===== Exercicis =====
- Crea una aplicació de tasques o **//tasklist//** utilitzant el ''localStorage'' com a sistema de persistència, tal i com s'explica més amunt.
- 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:
* Crida amb mètode GET amb paràmetres en format estàndard:curl http://localhost:5000/api/login -d "user=pepe&password=123"
* Crida GET amb paràmetres en la URL:curl http://localhost:5000/api/logincheck/pepe/123
* Crida POST i format JSON:curl -X POST localhost:5000/api/logincheck -d '{"nom":"pepe", "pass":"123"}' -H "Content-Type: application/json"
Fixa't en què:
* Adjuntem dades amb "-d"
* Indiquem el mètode amb -X
* Afegim //headers// amb -H, en aquest cas per indicar que les dades van en format JSON.
* ULL! **Si volem enviar les dades en format JSON, caldrà ajustar el body-parser de la següent manera**:app.use(bodyParser.json())
Teniu aquí [[https://reqbin.com/req/c-dwjszac0/curl-post-json-example|més exemples de com es fan les crides amb curl]].
[[http://expressjs.com/es/api.html#req|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:
* ''req.query'' per a les dades GET
* ''req.body'' per a les dades POST (amb body-parser)
* ''req.params'' per a les dades de la URL
* ''req.headers'' per a les dades del //headers// HTTP.
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 [[https://expressjs.com/en/resources/middleware/cors.html|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:
* **BasicAuth** : es tracta d'enviar usuari i password codificats en base64 a les capçaleres. No recomanat en entorns de producció ja que la contrasenya és una dada sensible que no convé enviar a cada request.
* **TokenAuth** : més recomanable que el BasicAuth, ja que enviarem la contrasenya 1 cop a l'inici de la comunicació, i se'ns retornarà un token, que podrem emprar més tard per cada request que fem. D'aquesta manera el nivell de seguretat es millora.
==== 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ó. [[https://www.npmjs.com/package/express-basic-auth|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 [[https://www.okta.com/identity-101/what-is-token-based-authentication/|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 [[https://www.section.io/engineering-education/how-to-build-authentication-api-with-jwt-token-in-nodejs/|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 [[https://socket.io/get-started/chat/|tutorial inicial per crear un programa de xat]] que il·lustra molt bé com treballar.
\\