Creación de un formulario de contacto sin servidor para su sitio estático

Publicado: 2022-03-10
Resumen rápido ↬ Con la ayuda de este artículo, finalmente podrá aprender los conceptos básicos de Amazon Web Services (AWS) Lambda y las API de Simple Email Service (SES) para ayudarlo a crear su propio sitio estático de correo en Serverless Framework. ¡Empecemos!

Los generadores de sitios estáticos proporcionan una alternativa rápida y sencilla a los sistemas de gestión de contenido (CMS) como WordPress. No hay configuración de servidor o base de datos, solo un proceso de compilación y HTML, CSS y JavaScript simples. Desafortunadamente, sin un servidor, es fácil alcanzar sus límites rápidamente. Por ejemplo, al agregar un formulario de contacto.

Con el auge de la arquitectura sin servidor, agregar un formulario de contacto a su sitio estático ya no tiene por qué ser la razón para cambiar a un CMS. Es posible obtener lo mejor de ambos mundos: un sitio estático con un back-end sin servidor para el formulario de contacto (que no necesita mantener). ¡Quizás lo mejor de todo es que en sitios de poco tráfico, como carteras, los altos límites de muchos proveedores sin servidor hacen que estos servicios sean completamente gratuitos!

En este artículo, aprenderá los conceptos básicos de Amazon Web Services (AWS) Lambda y las API de Simple Email Service (SES) para crear su propio sitio estático de correo en Serverless Framework. El servicio completo tomará los datos de forma enviados desde una solicitud AJAX, alcanzará el punto final de Lambda, analizará los datos para crear los parámetros SES, enviará la dirección de correo electrónico y devolverá una respuesta a nuestros usuarios. Lo guiaré a través de la configuración de Serverless por primera vez a través de la implementación. Debería tardar menos de una hora en completarse, ¡así que comencemos!

El formulario del sitio estático, que envía el mensaje al extremo de Lambda y devuelve una respuesta al usuario.
El formulario del sitio estático, que envía el mensaje al extremo de Lambda y devuelve una respuesta al usuario.
¡Más después del salto! Continúe leyendo a continuación ↓

Configuración

Existen requisitos previos mínimos para comenzar con la tecnología Serverless. Para nuestros propósitos, es simplemente un entorno de nodo con Yarn, el marco sin servidor y una cuenta de AWS.

Configuración del proyecto

El sitio web de Serverless Framework. Útil para instalación y documentación.
El sitio web de Serverless Framework. Útil para instalación y documentación.

Usamos Yarn para instalar Serverless Framework en un directorio local.

  1. Cree un nuevo directorio para alojar el proyecto.
  2. Navegue hasta el directorio en su interfaz de línea de comandos.
  3. Ejecute yarn init para crear un archivo package.json para este proyecto.
  4. Ejecute yarn add serverless para instalar el marco localmente.
  5. Ejecute yarn serverless create --template aws-nodejs --name static-site-mailer para crear una plantilla de servicio de nodo y asígnele el nombre static-site-mailer .

Nuestro proyecto está configurado, pero no podremos hacer nada hasta que configuremos nuestros servicios de AWS.

Configuración de una cuenta, credenciales y un servicio de correo electrónico simple de Amazon Web Services

La página de registro de Amazon Web Services, que incluye un generoso nivel gratuito, lo que permite que nuestro proyecto sea completamente gratuito.
La página de registro de Amazon Web Services, que incluye un generoso nivel gratuito, lo que permite que nuestro proyecto sea completamente gratuito.

Serverless Framework ha grabado un recorrido en video para configurar las credenciales de AWS, pero también he enumerado los pasos aquí.

  1. Regístrese para obtener una cuenta de AWS o inicie sesión si ya tiene una.
  2. En la barra de búsqueda de AWS, busque "IAM".
  3. En la página de IAM, haga clic en "Usuarios" en la barra lateral, luego en el botón "Agregar usuario".
  4. En la página Agregar usuario, asigne un nombre al usuario; algo como "sin servidor" es apropiado. Marque "Acceso programático" en Tipo de acceso y luego haga clic en Siguiente.
  5. En la pantalla de permisos, haga clic en la pestaña "Adjuntar políticas existentes directamente", busque "AdministratorAccess" en la lista, márquelo y haga clic en siguiente.
  6. En la pantalla de revisión, debería ver su nombre de usuario, con "Acceso programático" y "Acceso de administrador", luego cree el usuario.
  7. La pantalla de confirmación muestra la "ID de la clave de acceso" y la "Clave de acceso secreta" del usuario; las necesitará para proporcionar acceso a Serverless Framework. En su CLI, escriba yarn sls config credentials --provider aws --key YOUR_ACCESS_KEY_ID --secret YOUR_SECRET_ACCESS_KEY , reemplazando YOUR_ACCESS_KEY_ID y YOUR_SECRET_ACCESS_KEY con las claves en la pantalla de confirmación.

Sus credenciales están configuradas ahora, pero mientras estamos en la consola de AWS configuremos Simple Email Service.

  1. Haga clic en Console Home en la esquina superior izquierda para ir a casa.
  2. En la página de inicio, en la barra de búsqueda de AWS, busque "Servicio de correo electrónico simple".
  3. En la página de inicio de SES, haga clic en "Direcciones de correo electrónico" en la barra lateral.
  4. En la página de lista de direcciones de correo electrónico, haga clic en el botón "Verificar una nueva dirección de correo electrónico".
  5. En la ventana de diálogo, escriba su dirección de correo electrónico y luego haga clic en "Verificar esta dirección de correo electrónico".
  6. Recibirá un correo electrónico en unos momentos con un enlace para verificar la dirección. Haga clic en el enlace para completar el proceso.

Ahora que nuestras cuentas están hechas, echemos un vistazo a los archivos de plantilla de Serverless.

Configuración del marco sin servidor

Ejecutar la serverless create crea dos archivos: handler.js, que contiene la función Lambda, y serverless.yml, que es el archivo de configuración para toda la arquitectura sin servidor. Dentro del archivo de configuración, puede especificar tantos controladores como desee, y cada uno se asignará a una nueva función que puede interactuar con otras funciones. En este proyecto, solo crearemos un solo controlador, pero en una arquitectura sin servidor completa, tendría varias de las diversas funciones del servicio.

La estructura de archivos predeterminada generada a partir de Serverless Framework que contiene handler.js y serverless.yml.
La estructura de archivos predeterminada generada a partir de Serverless Framework que contiene handler.js y serverless.yml.

En handler.js, verá una sola función exportada llamada hello . Esta es actualmente la función principal (y única). Este, junto con todos los controladores de nodos, toma tres parámetros:

  • event
    Esto se puede considerar como los datos de entrada para la función.
  • context object
    Contiene la información de tiempo de ejecución de la función Lambda.
  • callback
    Un parámetro opcional para devolver información a la persona que llama.
 // handler.js 'use strict'; module.exports.hello = (event, context, callback) => { const response = { statusCode: 200, body: JSON.stringify({ message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }), }; callback(null, response); };

En la parte inferior de hello , hay una devolución de llamada. Es un argumento opcional para devolver una respuesta, pero si no se llama explícitamente , devolverá implícitamente un null . La devolución de llamada toma dos parámetros:

  • erro error
    Para proporcionar información de error para cuando el propio Lambda falla. Cuando Lambda tiene éxito, se debe pasar null a este parámetro.
  • resultado del objeto
    Para proporcionar un objeto de respuesta. Debe ser compatible con JSON.stringify . Si hay un parámetro en el campo de error, este campo se ignora.

Nuestro sitio estático enviará los datos de nuestro formulario en el cuerpo del evento y la devolución de llamada devolverá una respuesta para que nuestro usuario la vea.

En serverless.yml verá el nombre del servicio, la información del proveedor y las funciones.

 # serverless.yml service: static-site-mailer provider: name: aws runtime: nodejs6.10 functions: hello: handler: handler.hello 
Cómo los nombres de función en serverless.yml se asignan a handler.js.
Cómo los nombres de función en serverless.yml se asignan a handler.js.

¿Observe el mapeo entre la función hola y el controlador? Podemos nombrar nuestro archivo y función cualquier cosa y siempre que se asigne a la configuración, funcionará. Cambiemos el nombre de nuestra función a staticSiteMailer .

 # serverless.yml functions: staticSiteMailer: handler: handler.staticSiteMailer
 // handler.js module.exports.staticSiteMailer = (event, context, callback) => { ... };

Las funciones de Lambda necesitan permiso para interactuar con otra infraestructura de AWS. Antes de que podamos enviar un correo electrónico, debemos permitir que SES lo haga. En serverless.yml, debajo provider.iamRoleStatements , agregue el permiso.

 # serverless.yml provider: name: aws runtime: nodejs6.10 iamRoleStatements: - Effect: "Allow" Action: - "ses:SendEmail" Resource: ["*"]

Dado que necesitamos una URL para nuestra acción de formulario, debemos agregar eventos HTTP a nuestra función. En serverless.yml creamos una ruta, especificamos el método como post y establecemos CORS en verdadero por seguridad.

 functions: staticSiteMailer: handler: handler.staticSiteMailer events: - http: method: post path: static-site-mailer cors: true

Nuestros archivos serverless.yml y handler.js actualizados deberían verse así:

 # serverless.yml service: static-site-mailer provider: name: aws runtime: nodejs6.10 functions: staticSiteMailer: handler: handler.staticSiteMailer events: - http: method: post path: static-site-mailer cors: true provider: name: aws runtime: nodejs6.10 iamRoleStatements: - Effect: "Allow" Action: - "ses:SendEmail" Resource: ["*"]
 // handler.js 'use strict'; module.exports.staticSiteMailer = (event, context, callback) => { const response = { statusCode: 200, body: JSON.stringify({ message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }), }; callback(null, response); };

Nuestra arquitectura sin servidor está configurada, así que vamos a implementarla y probarla. Obtendrá una respuesta JSON simple.

 yarn sls deploy --verbose yarn sls invoke --function staticSiteMailer { "statusCode": 200, "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":{}}" } 
La respuesta de retorno al invocar nuestra nueva función sin servidor.
La respuesta de retorno al invocar nuestra nueva función sin servidor.

Crear el formulario HTML

La entrada de nuestra función Lambda y la salida del formulario deben coincidir, por lo que antes de compilar la función, compilaremos el formulario y capturaremos su salida. Lo mantenemos simple con campos de nombre, correo electrónico y mensaje. Agregaremos la acción del formulario una vez que hayamos implementado nuestra arquitectura sin servidor y obtenido nuestra URL, pero sabemos que será una solicitud POST para que podamos agregarla. Al final del formulario, agregamos una etiqueta de párrafo para mostrar mensajes de respuesta al usuario que actualizaremos en la devolución de llamada de envío.

 <form action="{{ SERVICE URL }}" method="POST"> <label> Name <input type="text" name="name" required> </label> <label> Email <input type="email" name="reply_to" required> </label> <label> Message: <textarea name="message" required></textarea> </label> <button type="submit">Send Message</button> </form> <p></p>

Para capturar el resultado, agregamos un controlador de envío al formulario, convertimos nuestros parámetros de formulario en un objeto y enviamos JSON en cadena a nuestra función Lambda. En la función Lambda usamos JSON.parse() para leer nuestros datos. Alternativamente, puede usar Serialize o query-string de jQuery para enviar y analizar los parámetros del formulario como una cadena de consulta, pero JSON.stringify() y JSON.parse() son nativos.

 (() => { const form = document.querySelector('form'); const formResponse = document.querySelector('js-form-response'); form.onsubmit = e => { e.preventDefault(); // Prepare data to send const data = {}; const formElements = Array.from(form); formElements.map(input => (data[input.name] = input.value)); // Log what our lambda function will receive console.log(JSON.stringify(data)); }; })();

Continúe y envíe su formulario y luego capture la salida de la consola. Lo usaremos en nuestra función Lambda a continuación.

Captura de los datos del formulario en un registro de la consola.
Captura de los datos del formulario en un registro de la consola.

Invocación de funciones de Lambda

Especialmente durante el desarrollo, necesitamos probar que nuestra función hace lo que esperamos. Serverless Framework proporciona el comando de invoke e invoke local para activar su función desde entornos en vivo y de desarrollo , respectivamente. Ambos comandos requieren que se pase el nombre de la función, en nuestro caso staticSiteMailer .

 yarn sls invoke local --function staticSiteMailer

Para pasar datos simulados a nuestra función, cree un nuevo archivo llamado data.json con la salida de la consola capturada bajo una clave de body dentro de un objeto JSON. Debería verse algo como:

 // data.json { "body": "{\"name\": \"Sender Name\",\"reply_to\": \"[email protected]\",\"message\": \"Sender message\"}" }

Para invocar la función con los datos locales, pase el argumento --path junto con la ruta al archivo.

 yarn sls invoke local --function staticSiteMailer --path data.json 
Una respuesta de retorno actualizada de nuestra función sin servidor cuando le pasamos datos JSON.
Una respuesta de retorno actualizada de nuestra función sin servidor cuando le pasamos datos JSON.

Verá una respuesta similar a la anterior, pero la tecla de input contendrá el evento del que nos burlamos. ¡Usemos nuestros datos simulados para enviar un correo electrónico usando Simple Email Service!

Enviar un correo electrónico con el servicio de correo electrónico simple

Vamos a reemplazar la función staticSiteMailer con una llamada a una función sendEmail privada. Por ahora, puede comentar o eliminar el código de la plantilla y reemplazarlo con:

 // hander.js function sendEmail(formData, callback) { // Build the SES parameters // Send the email } module.exports.staticSiteMailer = (event, context, callback) => { const formData = JSON.parse(event.body); sendEmail(formData, function(err, data) { if (err) { console.log(err, err.stack); } else { console.log(data); } }); };

Primero, analizamos el cuerpo del event.body para capturar los datos del formulario, luego lo pasamos a una función privada de envío de sendEmail electrónico. sendEmail es responsable de enviar el correo electrónico, y la función de devolución de llamada devolverá una respuesta de error o éxito con err o data . En nuestro caso, simplemente podemos registrar el error o los datos, ya que lo reemplazaremos con la devolución de llamada de Lambda en un momento.

Amazon proporciona un SDK conveniente, aws-sdk , para conectar sus servicios con las funciones de Lambda. Muchos de sus servicios, incluido SES, forman parte de él. Lo agregamos al proyecto con yarn add aws-sdk y lo importamos en la parte superior del archivo del controlador.

 // handler.js const AWS = require('aws-sdk'); const SES = new AWS.SES();

En nuestra función sendEmail privada, construimos los parámetros SES.sendEmail a partir de los datos del formulario analizados y usamos la devolución de llamada para devolver una respuesta a la persona que llama. Los parámetros requieren lo siguiente como objeto:

  • Fuente
    La dirección de correo electrónico desde la que envía SES.
  • Direcciones de respuesta
    Una matriz de direcciones de correo electrónico agregadas al campo de respuesta en el correo electrónico.
  • Destino
    Un objeto que debe contener al menos una ToAddresses , CcAddresses o BccAddresses . Cada campo toma una matriz de direcciones de correo electrónico que corresponden a los campos para , cc y bcc respectivamente.
  • Mensaje
    Un objeto que contiene el Cuerpo y el Sujeto .

Dado que formData es un objeto, podemos llamar a nuestros campos de formulario directamente como formData.message , crear nuestros parámetros y enviarlos. Pasamos su correo electrónico verificado por SES a Source y Destination.ToAddresses . Siempre que el correo electrónico esté verificado, puede pasar cualquier cosa aquí, incluidas diferentes direcciones de correo electrónico. Extraemos nuestro reply_to , message y name de nuestro objeto formData para completar los campos ReplyToAddresses y Message.Body.Text.Data .

 // handler.js function sendEmail(formData, callback) { const emailParams = { Source: '[email protected]', // SES SENDING EMAIL ReplyToAddresses: [formData.reply_to], Destination: { ToAddresses: ['[email protected]'], // SES RECEIVING EMAIL }, Message: { Body: { Text: { Charset: 'UTF-8', Data: `${formData.message}\n\nName: ${formData.name}\nEmail: ${formData.reply_to}`, }, }, Subject: { Charset: 'UTF-8', Data: 'New message from your_site.com', }, }, }; SES.sendEmail(emailParams, callback); }

SES.sendEmail enviará el correo electrónico y nuestra devolución de llamada devolverá una respuesta. Al invocar la función local, se enviará un correo electrónico a su dirección verificada.

 yarn sls invoke local --function staticSiteMailer --path data.json 
La respuesta de retorno de SES.sendEmail cuando tiene éxito.
La respuesta de retorno de SES.sendEmail cuando tiene éxito.

Devolver una respuesta del controlador

Nuestra función envía un correo electrónico usando la línea de comando, pero no es así como nuestros usuarios interactuarán con ella. Necesitamos devolver una respuesta a nuestro envío de formulario AJAX. Si falla, deberíamos devolver un código de err.message statusCode Cuando tiene éxito, el statusCode de estado 200 es suficiente, pero también devolveremos la respuesta del correo en el cuerpo. En staticSiteMailer construimos nuestros datos de respuesta y reemplazamos nuestra función de devolución de llamada sendEmail con la devolución de llamada de Lambda.

 // handler.js module.exports.staticSiteMailer = (event, context, callback) => { const formData = JSON.parse(event.body); sendEmail(formData, function(err, data) { const response = { statusCode: err ? 500 : 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'https://your-domain.com', }, body: JSON.stringify({ message: err ? err.message : data, }), }; callback(null, response); }); };

Nuestra devolución de llamada de Lambda ahora devuelve mensajes de éxito y de error de SES.sendEmail . Creamos la respuesta comprobando si err está presente para que nuestra respuesta sea consistente. La propia función de devolución de llamada de Lambda pasa null en el campo de argumento de error y la respuesta como segundo. Queremos pasar errores en adelante, pero si Lambda falla, su devolución de llamada se llamará implícitamente con la respuesta de error.

En los headers , deberá reemplazar Access-Control-Allow-Origin con su propio dominio. ¡Esto evitará que otros dominios utilicen su servicio y potencialmente acumulen una factura de AWS a su nombre! Y no lo cubro en este artículo, pero es posible configurar Lambda para usar su propio dominio. Deberá tener un certificado SSL/TLS cargado en Amazon. El equipo de Serverless Framework escribió un fantástico tutorial sobre cómo hacerlo.

Invocar la función local ahora enviará un correo electrónico y devolverá la respuesta adecuada.

 yarn sls invoke local --function staticSiteMailer --path data.json 
La respuesta de retorno de nuestra función sin servidor, que contiene la respuesta de retorno SES.sendEmail en el cuerpo.
La respuesta de retorno de nuestra función sin servidor, que contiene la respuesta de retorno SES.sendEmail en el cuerpo.

Llamar a la función Lambda desde el formulario

¡Nuestro servicio es completo! Para implementarlo, ejecute yarn sls deploy -v . Una vez que se implemente, obtendrá una URL similar a https://r4nd0mh45h.execute-api.us-east-1.amazonaws.com/dev/static-site-mailer que puede agregar a la acción del formulario. A continuación, creamos la solicitud AJAX y devolvemos la respuesta al usuario.

 (() => { const form = document.querySelector('form'); const formResponse = document.querySelector('js-form-response'); form.onsubmit = e => { e.preventDefault(); // Prepare data to send const data = {}; const formElements = Array.from(form); formElements.map(input => (data[input.name] = input.value)); // Log what our lambda function will receive console.log(JSON.stringify(data)); // Construct an HTTP request var xhr = new XMLHttpRequest(); xhr.open(form.method, form.action, true); xhr.setRequestHeader('Accept', 'application/json; charset=utf-8'); xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); // Send the collected data as JSON xhr.send(JSON.stringify(data)); // Callback function xhr.onloadend = response => { if (response.target.status === 200) { // The form submission was successful form.reset(); formResponse.innerHTML = 'Thanks for the message. I'll be in touch shortly.'; } else { // The form submission failed formResponse.innerHTML = 'Something went wrong'; console.error(JSON.parse(response.target.response).message); } }; }; })();

En la devolución de llamada de AJAX, verificamos el código de estado con response.target.status . Si es diferente a 200 , podemos mostrar un mensaje de error al usuario; de lo contrario, infórmele que se envió el mensaje. Dado que nuestro Lambda devuelve JSON en cadena, podemos analizar el mensaje del cuerpo con JSON.parse(response.target.response).message . Es especialmente útil para registrar el error.

¡Debería poder enviar su formulario completamente desde su sitio estático!

El formulario del sitio estático, que envía el mensaje al extremo de Lambda y devuelve una respuesta al usuario.
El formulario del sitio estático, que envía el mensaje al extremo de Lambda y devuelve una respuesta al usuario.

Próximos pasos

Agregar un formulario de contacto a su estática es fácil con Serverless Framework y AWS. Hay margen de mejora en nuestro código, como agregar validación de formularios con un honeypot, evitar llamadas AJAX para formularios no válidos y mejorar la experiencia de usuario si la respuesta, pero esto es suficiente para comenzar. Puede ver algunas de estas mejoras en el repositorio de correo del sitio estático que he creado. ¡Espero haberte inspirado para que pruebes Serverless tú mismo!