11 - Implementación de un libro de visitas mediante un archivo de texto


En el concepto anterior vimos como podemos capturar los datos de un formulario HTML para su procesamiento en el servidor.

Problema

Implementar un sitio web que permita mediante un formulario HTML ingresar el nombre y comentarios del visitante del sitio. Por otro lado imprimir todos los comentarios que dejan los visitantes del sitio. Almacenar los comentarios de los visitantes en un archivo de texto.

Crear una carpeta llamada ejercicio13 y en su interior crearemos el archivo ejercicio13.js (con el programa en Node.js propiamente dicho) y una carpeta llamada public. En la carpeta public crear dos archivos html con un menú y un formulario.

archivos proyecto con Node.js

El contenido del archivo:

index.html
<!doctype html>
<html>
<head>
  <title>Prueba</title>
</head>
<body>
   <a href="cargarcomentario.html">Cargar comentarios en el libro de visitas.</a></p>
   <a href="leercomentarios">Ver comentarios del libro de visitas.</a></p>  
</body>
</html>

En este primer archivo HTML podemos hacer notar que el segundo enlace tiene referencia a una URL que será capturada por nuestro programa en Node.js y procederemos a generar dinámicamente una página con el contenido del archivo de texto con todos los comentarios cargados hasta ese momento.

cargarcomentario.html
<!DOCTYPE html>
<html>
<head>
  <title>Libro de visitas</title>
</head>
<body>
  <form action="cargar" method="post">
  Ingrese su nombre:
  <input type="text" name="nombre" size="30"><br>
  Comentarios:<br>
  <textarea name="comentarios" rows="5" cols="60"></textarea> 
  <br>
  <input type="submit" value="Enviar">
</form>
</body>
</html>

Como vimos en el concepto anterior la propiedad action del formulario tiene el valor que capturaremos en nuestro programa en Node.js

Ahora pasaremos a codificar el archivo que contiene el programa en Node.js:

ejercicio13.js
const http = require('node:http')
const fs = require('node:fs')

const mime = {
  'html': 'text/html',
  'css': 'text/css',
  'jpg': 'image/jpg',
  'ico': 'image/x-icon',
  'mp3': 'audio/mpeg3',
  'mp4': 'video/mp4'
}

const servidor = http.createServer((pedido, respuesta) => {
  const url = new URL('http://localhost:8888' + pedido.url)
  let camino = 'public' + url.pathname
  if (camino == 'public/')
    camino = 'public/index.html'
  encaminar(pedido, respuesta, camino)
})
servidor.listen(8888)


function encaminar(pedido, respuesta, camino) {
  switch (camino) {
    case 'public/cargar': {
      grabarComentarios(pedido, respuesta)
      break
    }
    case 'public/leercomentarios': {
      leerComentarios(respuesta)
      break
    }
    default: {
      fs.stat(camino, error => {
        if (!error) {
          fs.readFile(camino, (error, contenido) => {
            if (error) {
              respuesta.writeHead(500, { 'Content-Type': 'text/plain' })
              respuesta.write('Error interno')
              respuesta.end()
            } else {
              const vec = camino.split('.')
              const extension = vec[vec.length - 1]
              const mimearchivo = mime[extension]
              respuesta.writeHead(200, { 'Content-Type': mimearchivo })
              respuesta.write(contenido)
              respuesta.end()
            }
          })
        } else {
          respuesta.writeHead(404, { 'Content-Type': 'text/html' })
          respuesta.write('<!doctype html><html><head></head><body>Recurso inexistente</body></html>')
          respuesta.end()
        }
      })
    }
  }
}


function grabarComentarios(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  });
  pedido.on('end', function () {
    const formulario = new URLSearchParams(info)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    const pagina = `<!doctype html><html><head></head><body>
                Nombre:${formulario.get('nombre')}<br>
                Comentarios:${formulario.get('comentarios')}<br>
                <a href="index.html">Retornar</a>
                </body></html>`
    respuesta.end(pagina)
    grabarEnArchivo(formulario)
  })
}

function grabarEnArchivo(formulario) {
  const datos = `nombre:${formulario.get('nombre')}<br>
               comentarios:${formulario.get('comentarios')}<hr>`
  fs.appendFile('public/visitas.txt', datos, error => {
    if (error)
      console.log(error)
  })
}

function leerComentarios(respuesta) {
  fs.readFile('public/visitas.txt', (error, datos) => {
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    respuesta.write('<!doctype html><html><head></head><body>')
    respuesta.write(datos)
    respuesta.write('</body></html>')
    respuesta.end()
  })
}

console.log('Servidor web iniciado')

Todo la primera parte de nuestro programa es idéntica. Lo primero que cambia es cuando tenemos que enrutar los pedidos según la url:

function encaminar(pedido, respuesta, camino) {
  switch (camino) {
    case 'public/cargar': {
      grabarComentarios(pedido, respuesta)
      break
    }
    case 'public/leercomentarios': {
      leerComentarios(respuesta)
      break
    }
    default: {
      // sirve para las estáticas
    }
  }	
}

Si presionamos el botón submit del formulario HTML procede a verificarse verdadero el primer case del switch. Llamamos al método grabarComentarios

La función grabarComentarios rescata primeramente todos los datos del formulario los parsea, los muestra en una página HTML (esto es todo lo visto en el concepto anterior) y procedemos a llamar a la función grabarEnArchivo:

function grabarComentarios(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  });
  pedido.on('end', function () {
    const formulario = new URLSearchParams(info)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    const pagina = `<!doctype html><html><head></head><body>
                Nombre:${formulario.get('nombre')}<br>
                Comentarios:${formulario.get('comentarios')}<br>
                <a href="index.html">Retornar</a>
                </body></html>`
    respuesta.end(pagina)
    grabarEnArchivo(formulario)
  })
}

La función grabarEnArchivo concatenamos los datos a grabar y llamamos al método appendFile que crea el archivo de texto 'visitas.txt' o lo abre para agregar en el caso que ya exista. El segundo parámetro recibe el string a grabar en el archivo de texto:

function grabarEnArchivo(formulario) {
  const datos = `nombre:${formulario.get('nombre')}<br>
               comentarios:${formulario.get('comentarios')}<hr>`
  fs.appendFile('public/visitas.txt', datos, error => {
    if (error)
      console.log(error)
  })
}

Recordemos que primero debemos iniciar el servidor desde la consola del sistema operativo tipeando:

c:\ejerciciosnodejs\ejercicio13>node ejercicio13.js

Luego en el navegador seleccionamos la primer opción:

menu html

Cargamos algunos datos en el formulario HTML (recordemos que es un archivo HTML estático):

formulario html

Finalmente cuando se suben los datos al servidor quedan registrados en el archivo de texto (que es lo nuevo que estamos viendo en este concepto) y se devuelve al navegador los datos ingresados:

formulario html

Para la impresión de datos desde la página index.html tenemos el siguiente enlace:

   <a href="leercomentarios">Ver comentarios del libro de visitas.</a></p>  

Esto hace que en el case de la función encaminar procedemos a llamar a la función leerComentarios:

    case 'public/leercomentarios': {
      leerComentarios(respuesta)
      break
    }

La función leerComentarios utiliza el objeto 'fs' para leer el archivo de texto y generar una página dinámica con dichos datos:

function leerComentarios(respuesta) {
  fs.readFile('public/visitas.txt', (error, datos) => {
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    respuesta.write('<!doctype html><html><head></head><body>')
    respuesta.write(datos)
    respuesta.write('</body></html>')
    respuesta.end()
  })
}

El resultado en pantalla cuando pedimos dicho recurso desde el navegador es:

listado archivo de texto desde Node.js

Este proyecto lo puede descargar en un zip con todos los archivos desde este enlace : ejercicio13

Implementación alternativa empleando el módulo 'fs/promises'

ejercicio13b.js
const http = require('node:http')
const fs = require('node:fs/promises')

const mime = {
  'html': 'text/html',
  'css': 'text/css',
  'jpg': 'image/jpg',
  'ico': 'image/x-icon',
  'mp3': 'audio/mpeg3',
  'mp4': 'video/mp4'
}

const servidor = http.createServer((pedido, respuesta) => {
  const url = new URL('http://localhost:8888' + pedido.url)
  let camino = 'public' + url.pathname
  if (camino == 'public/')
    camino = 'public/index.html'
  encaminar(pedido, respuesta, camino)
})
servidor.listen(8888)


function encaminar(pedido, respuesta, camino) {
  switch (camino) {
    case 'public/cargar': {
      grabarComentarios(pedido, respuesta)
      break
    }
    case 'public/leercomentarios': {
      leerComentarios(respuesta)
      break
    }
    default: {
      fs.stat(camino)
        .then(() => {
          fs.readFile(camino)
            .then(contenido => {
              const vec = camino.split('.')
              const extension = vec[vec.length - 1]
              const mimearchivo = mime[extension]
              respuesta.writeHead(200, { 'Content-Type': mimearchivo })
              respuesta.write(contenido)
              respuesta.end()
            })
            .catch(error => {
              respuesta.writeHead(500, { 'Content-Type': 'text/plain' })
              respuesta.write('Error interno')
              respuesta.end()
            })
        })
        .catch(error => {
          respuesta.writeHead(404, { 'Content-Type': 'text/html' })
          respuesta.end('<h1>Error 404: No existe el recurso solicitado</h1>')
        })
    }
  }
}


function grabarComentarios(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  });
  pedido.on('end', function () {
    const formulario = new URLSearchParams(info)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    const pagina = `<!doctype html><html><head></head><body>
                Nombre:${formulario.get('nombre')}<br>
                Comentarios:${formulario.get('comentarios')}<br>
                <a href="index.html">Retornar</a>
                </body></html>`
    respuesta.end(pagina)
    grabarEnArchivo(formulario)
  })
}

function grabarEnArchivo(formulario) {
  const datos = `nombre:${formulario.get('nombre')}<br>
               comentarios:${formulario.get('comentarios')}<hr>`
  fs.appendFile('public/visitas.txt',datos)
  .then(() => {
    console.log('datos grabados')
  })
  .catch(error => {
    console.log('error al grabar datos')
  })
}

function leerComentarios(respuesta) {
  fs.readFile('public/visitas.txt')
    .then(contenido => {
      respuesta.writeHead(200, { 'Content-Type': 'text/html' })
      respuesta.write('<!doctype html><html><head></head><body>')
      respuesta.write(contenido)
      respuesta.write('</body></html>')
      respuesta.end()
    })
    .catch(error => {
      respuesta.writeHead(500, { 'Content-Type': 'text/plain' })
      respuesta.write('Error interno')
      respuesta.end()
    })
}

console.log('Servidor web iniciado')

Retornar