![](https://habrastorage.org/webt/dl/r1/h_/dlr1h_rq21rxqaho-fl8fpfzjfm.png)
Это вторая часть туториала, посвященного реализации Real World App
— подписки на обновления с помощью гугл таблиц, бессерверных функций и реакта.
Напомню, что основной функционал нашего приложения, который мы реализовали в первой части туториала, является следующим:
- на главной странице отображается приветствие и предложение подписаться на обновления
- при нажатии на кнопку «Подписаться», пользователь попадает на страницу с формой, содержащей два поля: имя и адрес электронной почты
- для защиты от ботов используется гугл рекапча 2 версии
- при заполнении полей и прохождения проверки снимается блокировка с кнопки «Подписаться»
- при нажатии этой кнопки данные пользователя отправляются в таблицу с помощью бессерверной функции
Дополнительный функционал, реализацией которого мы займемся в этой части:
- с помощью скрипта осуществляется автоматическая рассылка уведомлений
- в рассылаемых письмах содержится ссылка на отписку от обновлений
- при переходе по этой ссылке адрес электронной почты передается бессерверной функции, с помощью которой из таблиц удаляется соответствующая строка
Демо приложения, разработкой которого мы занимаемся, можно посмотреть здесь (оно вполне работоспособное, если хотите, можете подписаться на обновления).
Код приложения находится здесь.
Для реализации приложения используются следующие технологии:
netlify-cli
— интерфейс командной строки для запуска сервера для разработки (инициализации бессерверных функций) и "деплоя" приложения наNetlify
; требуется глобальная установка:yarn global add netlify-cli
илиnpm i -g netlify-cli
; обязательноgoogle-spreadsheet
— JavaScript-библиотека для работы с гугл таблицами; обязательноreact
— на мой взгляд, это лучший JavaScript-фреймворк для фронтенда, но вы можете использовать любую другую библиотеку; наши бессерверные функции не зависят от конкретного фреймворкаreact-router-dom
— React-библиотека для маршрутизацииsemantic-ui-react
— React-CSS-фреймворкreact-google-recaptcha
— React-компонент, позволяющий напрямую взаимодействовать с соответствующим сервисомnodemailer
— наиболее популярная Node.js-библиотека для работы с электронной почтой (рассылки писем)dotenv
— утилита для доступа к переменным среды окружения
Начнем с деплоя приложения на Netlify
.
Деплой приложения
Заходим на Netlify
, создаем аккаунт, затем вводим в терминале следующую команду:
netlify login
Вводим логин и пароль, получаем сообщение об успешной авторизации.
Выполняем сборку проекта:
yarn build
# или
npm run build
И разворачиваем приложение в тестовом режиме:
netlify deploy
Отвечаем на вопросы (новое приложение, название приложения (например, mail-list
), директория для деплоя (build
) и т.д.), получаем ссылку на развернутое приложение.
Переходим по ссылке, видим, что приложение не работает. Почему? Потому что мы не добавили переменные среды окружения.
Переходим в раздел sites
, открываем наше приложение, выбираем вкладку Site settings
, затем вкладку Build & deploy
, находим раздел Environment
, добавляем переменные (Environment variables
).
![](https://habrastorage.org/getpro/habr/post_images/796/b68/536/796b6853654d9dc720f04d7b59e5c8f2.png)
![](https://habrastorage.org/getpro/habr/post_images/679/3b8/1f7/6793b81f799a9409ed518f296e463e5e.png)
Не будем ходить вокруг да около, а сразу развернем приложение в продакшн-режиме:
netlify deploy --prod
Готово. Легко, правда? Вот за что я люблю Netlify
.
Теперь, когда у нас имеется URL
, мы можем зарегистрировать наше приложение в Google ReCAPTCHA
.
Заходим в консоль администратора и создаем новое приложение (+
). Вводим название сайта (ярлык), выбираем reCAPTCHA v2
, указываем домен (URL
нашего приложения без протокола), принимаем условия использования (флажок "Отправлять владельцам оповещения" можно снять), нажимаем "Отправить". Получаем ключ сайта и секретный ключ, нам нужен только первый.
![](https://habrastorage.org/getpro/habr/post_images/982/a5f/a43/982a5fa432afe9b69c0ab6f83a206141.png)
![](https://habrastorage.org/getpro/habr/post_images/5d9/b80/001/5d9b8000180021973b533f2f8d5d2d12.png)
![](https://habrastorage.org/getpro/habr/post_images/2a5/584/2bc/2a55842bc1d97a40db8a0626b94b8cc1.png)
Добавляем в .env
такую переменную:
REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY=«YOUR_SITE_KEY»
Вносим изменение в Subscribe.js
:
<ReCAPTCHA
sitekey={process.env.REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY}
onChange={() => setRecaptcha(true)}
/>
Повторно собираем и разворачиваем приложение:
yarn build
# или
npm run build
# и
netlify deploy --prod
Если все сделано правильно, то на странице с формой появится «настоящая» капча.
![](https://habrastorage.org/getpro/habr/post_images/5f0/0d7/1ed/5f00d71ed7946bf2e6fb0348a9d5fb3b.png)
Отлично.
Переходим к автоматизации рассылки уведомлений и реализации возможности отписаться от обновлений.
Автоматическая рассылка уведомлений
Давайте подумаем о том, что нам нужно для того, чтобы пользователь имел возможность отписаться от обновлений.
Мы, например, могли бы добавлять в уведомление специальную ссылку, при переходе по которой выполняется отписка. Для идентификации пользователя, выразившего такое желание, нам нужен его адрес электронной почты. Следовательно, ссылка должна формироваться из пути к соответствующей странице нашего приложения + email пользователя (для того, чтобы мы могли извлекать его из параметров строки запроса).
Для реализации автоматической рассылки уведомлений мы будем использовать nodemailer
, а для тестирования — Mailtrap
.
Создаем аккаунт на Mailtrap
, открываем автоматически созданный проект MyInbox
, на вкладке SMTP Settings
в разделе Integrations
выбираем Node.js -> Nodemailer
, получаем данные для авторизации.
![](https://habrastorage.org/getpro/habr/post_images/e46/763/94f/e4676394f30033284d9ca113444c3d8c.png)
![](https://habrastorage.org/getpro/habr/post_images/533/35b/ce4/53335bce442fb9796b47a91cf5c337bc.png)
Сохраняем эти данные в .env
:
SMTP_USER='USER'
SMTP_PASS='PASS'
В корне проекта создаем директорию send-mail
, а в ней — index.js
следующего содержания:
require('dotenv').config()
const nodemailer = require('nodemailer')
const { GoogleSpreadsheet } = require('google-spreadsheet')
const doc = new GoogleSpreadsheet(process.env.GOOGLE_SPREADSHEET_ID)
// Тестовый транспортер для отправки сообщений
const testTransporter = nodemailer.createTransport({
host: 'smtp.mailtrap.io',
port: 2525,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
})
// Функция для создания сообщения в формате HTML
// Она принимает имя пользователя и его email
// Обратите внимание на значение атрибута `href` тега `a` -
// URL соответствующей страницы нашего приложения (скоро мы ее создадим) + email пользователя
const createMessage = (username, email) => `
<p>
<strong>Уважаемый ${username} </strong>, <em>спасибо за подписку</em>!
</p>
<p>
Для того, чтобы отписаться от обновлений, перейдите по <a href="https://mail-list.netlify.app/unsubscribe/${email}" target="_blank">этой ссылке</a>
</p>
`
const sendMail = async () => {
try {
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n')
})
await doc.loadInfo()
const sheet = doc.sheetsByIndex[0]
const rows = await sheet.getRows()
// Перебираем строки таблицы — данные пользователей,
// создаем сообщение и отправляем его
// text — резервный контент на случай, если почтовый клиент пользователя не поддерживаем сообщения в формате HTML
rows.forEach(async (row) => {
await testTransporter.sendMail({
from: 'Mail list <mail-list.netlify.app>',
to: row.email,
subject: 'Благодарность за подписку',
text: 'Спасибо за подписку',
html: createMessage(row.username, row.email)
})
})
console.log('Сообщения отправлены')
} catch (err) {
console.error(err)
}
}
sendMail()
Добавим в package.json
(раздел scripts
) команду для рассылки уведомлений:
«send»: «node send-mail/index.js
Запускаем скрипт (разумеется, в таблице должны быть какие-то данные):
yarn send
# или
npm run send
Получаем «Сообщения отправлены» в терминале и письмо в Mailtrap
.
![](https://habrastorage.org/getpro/habr/post_images/4be/e15/7d2/4bee157d271a9a1264d0fc6d8d482e99.png)
Для взаимодействия с реальными почтовыми службами (yahoo
в моем случае) нужен реальный SMTP-провайдер.
Среди наиболее популярных решений можно назвать SendGrid
и SendingBlue
, но в случае выбора одного из этих сервисов, нам придется долго и упорно убеждать их владельцев в том, что мы не собираемся заниматься рассылкой спама. Еще есть Mailgun
, но он платный с трехмесячным free trial.
Поэтому мы будем использовать Gmail
.
Безусловно, если очень хочется, можно поднять собственный SMPT-сервер. Также существуют инструменты для рассылки писем, которые, как заявляют их разработчики, работают без SMTP, например, sendmail
.
Добавляем в .env
переменные с данными вашего Gmail-аккаунта:
GMAIL_USER='USER'
GMAIL_PASS='PASS'
И вносим изменения в send-mail/index.js
:
/*
const testTransporter = nodemailer.createTransport({
host: 'smtp.mailtrap.io',
port: 2525,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
})
*/
const gmailTransporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.GMAIL_USER,
pass: process.env.GMAIL_PASS
}
})
rows.forEach(async (row) => {
await gmailTransporter.sendMail({
// ...
})
})
Запускаем скрипт (в таблице должен быть указан ваш email):
yarn send
![](https://habrastorage.org/getpro/habr/post_images/607/257/635/60725763550b7d2f0ae9bded9d6e7fbc.png)
Существует один нюанс, связанный с использованием Gmail
в качестве сервиса для рассылки писем — гугл может блокировать к нему доступ, считая приложение ненадежным (существует платная версия Gmail
— Google Workspace
, которая с точки зрения гугла, конечно же, является надежной).
Для того, чтобы снять блокировку, заходим в настройки гугл аккаунта, открываем вкладку «Безопасность», переходим в раздел «Ненадежные приложения, у которых есть доступ к аккаунту» и разрешаем небезопасные приложения.
![](https://habrastorage.org/getpro/habr/post_images/b3e/082/64f/b3e08264fc1cd725a46129482931d603.png)
С реализацией автоматической рассылки мы закончили, но при переходе по ссылке, указанной в письме, мы попадаем на резервную страницу, поскольку в нашем приложении нет страницы для отписки от обновлений, как и соответствующей бессерверной функции. Давайте это исправим.
Отписка от обновлений
Добавляем в приложение (src/pages
) новую страницу Unsubscribe.js
. На этой странице после скрытия индикатора загрузки, мы пытаемся получить email пользователя из параметров строки запроса с помощью хука useParams
. Если email отсутствует, выполняется перенаправление на главную страницу. Иначе мы отправляем email в функцию, которая удаляет из таблицы соответствующую строку. Если пользователь с указанным email не оформлял подписку на обновления, выбрасывается исключение. При успешном завершении операции отображается сообщение о том, что пользователь больше не будет получать уведомлений.
import { useState, useEffect } from 'react'
import { Link, useParams, useHistory } from 'react-router-dom'
import { Container, Button } from 'semantic-ui-react'
import { Spinner, useDeferredRoute } from '../hooks'
function Unsubscribe() {
const { loading } = useDeferredRoute(1000)
const [error, setError] = useState(null)
// Извлекаем email из параметров строки запроса
const { email } = useParams()
const history = useHistory()
useEffect(() => {
// Если email отсутствует, выполняем перенаправление на главную страницу
if (!email) {
return history.push('/')
}
async function unsubscribe() {
try {
// Отправляем email в функцию
const response = await fetch('/.netlify/functions/unsubscribe', {
method: 'POST',
body: JSON.stringify(email),
headers: {
'Content-Type': 'application/json'
}
})
// Если возникла ошибка, значит, пользователь не оформлял подписку
if (!response.ok) {
const json = await response.json()
setError(json.error)
}
} catch (err) {
console.error(err)
}
}
unsubscribe()
// eslint-disable-next-line
}, [])
if (loading) return <Spinner />
return (
<Container>
{error ? (
<h3>{error}</h3>
) : (
<h3>Вы больше не будете получать уведомлений</h3>
)}
<Button color='teal' as={Link} to='/'>
На главную
</Button>
</Container>
)
}
export default Unsubscribe
Все, что нам осталось сделать, это реализовать функцию unsubscribe
. Она очень похожа на функцию subscribe
— загружается таблица, выполняется поиск и удаление соответствующей строки:
require('dotenv').config()
const { GoogleSpreadsheet } = require('google-spreadsheet')
exports.handler = async (event) => {
const doc = new GoogleSpreadsheet(process.env.GOOGLE_SPREADSHEET_ID)
try {
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n')
})
await doc.loadInfo()
const sheet = doc.sheetsByIndex[0]
// Получаем email пользователя
const data = JSON.parse(event.body)
const rows = await sheet.getRows()
// Выполняем поиск соответствующей строки
const index = rows.findIndex((row) => row.email === data)
// Если строка не найдена, значит, пользователь не оформлял подписку
if (index === -1) {
const response = {
statusCode: 400,
body: JSON.stringify({
error: 'Пользователь с указанным email не найден'
}),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
}
}
return response
}
// Удаляем строку
await rows[index].delete()
const response = {
statusCode: 200,
body: JSON.stringify({ message: 'Пользователь удален' }),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
}
}
return response
} catch (err) {
console.error(err)
const response = {
statusCode: 500,
body: JSON.stringify({ error: 'Что-то пошло не так. Попробуйте позже' }),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
}
}
return response
}
}
Еще раз (обещаю, что в последний) собираем и разворачиваем проект:
yarn build
# или
npm run build
# и
netlify deploy --prod
Не забудьте обновить переменные среды окружения на Netlify
.
Теперь при переходе по ссылке, указанной в письме, пользователь попадает на страницу с сообщением о том, что он успешно отписался от обновлений, а в таблице удаляется соответствующая строка. При переходе на данную страницу без указания email выполняется перенаправление на главную страницу, а при указании несуществующего email — отображается сообщение о том, что пользователь с таким email не найден.
![](https://habrastorage.org/getpro/habr/post_images/2f4/88a/59f/2f488a59f56f484022f1f376dc0b3237.png)
![](https://habrastorage.org/getpro/habr/post_images/268/687/f2a/268687f2a5af9b22bba4d5634f0ebd42.png)
Туториал получился гораздо больше, чем я изначально планировал, но, надеюсь, вы не жалеете потраченного времени. Я постарался максимально подробно осветить все аспекты, связанные с разработкой нашего приложения, прошу меня извинить, если что-то упустил.
Как бы то ни было, если вы в точности следовали инструкциям, а еще лучше — реализовали какие-то дополнительные возможности, то в вашем портфолио появилось настоящее Real World App
, разработанное с использованием самых современных технологий (да, мы не использовали TypeScript
, но для нашего небольшого проекта это было бы слишком круто).
Облачные серверы от Маклауд быстрые и безопасные.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!