Русский
Русский
English
Статистика
Реклама

Подписка

Реализация подписки на обновления с помощью Google Sheets, Netlify Functions и React. Часть 1

04.06.2021 10:04:57 | Автор: admin

В этом туториале мы реализуем ~~Real World App~~ подписку на обновления с помощью гугл таблиц, бессерверных функций и реакта.


Основной функционал нашего приложения будет следующим:


  • на главной странице отображается приветствие и предложение подписаться на обновления
  • при нажатии на кнопку "Подписаться", пользователь попадает на страницу с формой, содержащей два поля: имя и адрес электронной почты
  • для защиты от ботов используется гугл рекапча 2 версии
  • при заполнении полей и прохождения проверки разблокируется кнопка "Подписаться"
  • при нажатии этой кнопки данные пользователя отправляются в таблицу с помощью бессерверной функции

Дополнительный функционал:


  • с помощью скрипта осуществляется автоматическая рассылка уведомлений
  • в рассылаемых письмах содержится ссылка на отписку от обновлений
  • при переходе по этой ссылке адрес электронной почты передается бессерверной функции, с помощью которой из таблиц удаляется соответствующая строка пользователь больше не будет получать уведомлений

В первой части туториала мы реализуем основной функционал, во второй дополнительный.


Сначала мы реализуем основной функционал локально, затем развернем приложение на Netlify (в данном случае мы ограничены этим хостингом, поскольку от него зависит ключевой функционал приложения) и будет его постепенно дорабатывать.


Демо приложения, которое мы создадим, можно посмотреть здесь (оно вполне работоспособное, если хотите, можете подписаться на обновления).


Код приложения находится здесь.


Для реализации приложения мы будем использовать следующие технологии:


  • 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 утилита для доступа к переменным среды окружения

Разумеется, на вашей машине должен быть установлен Node.js и, желательно, yarn (после того, как вы поработаете с этим пакетным менеджером, вы едва ли вернетесь к npm).


Также будьте готовы к тому, что придется создать несколько аккаунтов и, возможно, кое-что погуглить в случае, если я забуду об этом упомянуть в туториале (постараюсь этого не делать).


Надеюсь, мне удалось вас заинтриговать, и вы с нетерпением ждете, когда мы начнем писать код. Скоро, но сначала давайте создадим и настроим таблицу, а также свяжем ее с Google Cloud Platform.


Подготовка таблицы


Заходим в Google Cloud Platform (по ссылке, приведенной выше) и выполняем следующие действия:


  • создаем новый проект под названием, например, mail-list
  • ожидаем завершения создания проекта и выбираем его
  • переходим к обзору API (Go to APIs overview)
  • включаем Google Sheets API (Enable APIs and services)
  • создаем сервис-аккаунт для доступа к API (Create credentials)
  • переходим в созданный сервис-аккаунт
  • открываем вкладку Keys и добавляем ключ в формате JSON (Add key -> Create new key)
  • в скачанном файле (например, mail-list-315211-ca347b50f56a.json) нас интересуют свойства private_key и client_email; сохраните их где-нибудь, позже мы запишем их в переменные среды окружения

.


.


.


.


.


.


.


.


.


.


.


.


.


Заходим в Google Speadsheets и создаем новую таблицу (Пустой файл) с двумя графами: username и email.


.


Открываем настройки доступа, находим созданный нами сервис-аккаунт и добавляем его в качестве Редактора.


.


В поисковой строке между d/ и /edit находится идентификатор таблицы, также где-нибудь его сохраните.


На этом настройка нашей таблицы завершена.


Бессерверные функции


Приложение, реализацией которого мы занимаемся, представляет собой отличный пример использования бессерверных вычислений: нам не нужен полноценный сервер, мы всего лишь хотим отправлять данные из заполненной пользователем формы в гугл таблицу, читать эти данные и рассылать уведомления подписчикам. Netlify Functions это лишь один из вариантов использования AWS Lambda Functions для выполнения такого рода задач.


О том, что такое Netlify Functions, можно почитать здесь.


Функции, как правило, размещаются в директории functions в корне проекта. Создаем новый React-проект (mail-list название нашего проекта):


yarn create react-app mail-list# илиnpx create ...

Пьем чай в ожидании завершения создания проекта, переходим в созданную директорию и устанавливаем необходимые зависимости:


cd mail-listyarn add google-spreadsheet dotenv# илиnpm i ...

В корне проекта создаем файл .env (touch .env) и записываем в него сохраненные данные в следующем формате:


GOOGLE_SERVICE_ACCOUNT_EMAIL="YOUR_CLIENT_EMAIL"GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY----- YOUR_PRIVATE_KEY -----END PRIVATE KEY-----\n"GOOGLE_SPREADSHEET_ID="YOUR_SPREADSHEET_ID"

Создаем директорию functions, переходим в нее, создаем файл subscribe.js и открываем его в редакторе кода:


mkdir functionscd !$touch subscribe.jscode subscribe.js

В данном файле мы получаем доступ к сервис-аккаунту, загружаем таблицу, получаем данные от клиента (имя и адрес электронной почты пользователя) и записываем эти данные в таблицу. Перед записью данных пользователя, мы проверяем, что указанный пользователем email отсутствует в таблице. Если это не так, то мы сообщаем пользователю, что он уже подписан на обновления.


// Загружаем переменные среды окружения из файла ".env"require('dotenv').config()const { GoogleSpreadsheet } = require('google-spreadsheet')// Бессерверная функция (о ее сигнатуре мы поговорим позже)// В данном случае, нас интересует только первый аргумент, принимаемый функцией - `event`// `event` - это тоже самое, что `req` в `express`, т.е. объект запроса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]    // Получаем данные от клиента в формате JSON и преобразуем их в объект    const data = JSON.parse(event.body)    // Получаем строки таблицы    const rows = await sheet.getRows()    // Обратите внимание, что заголовки столбцов таблицы становятся одноименными свойствами строк    // Если какая-либо из строк содержит email, указанный пользователем,    // значит, пользователь уже оформил подписку на обновления    if (rows.some((row) => row.email === data.email)) {      // Формируем ответ      const response = {        statusCode: 400,        body: JSON.stringify({          error: 'Пользователь с таким email уже оформил подписку'        }),        // Про это поговорим позже        headers: {          'Access-Control-Allow-Origin': '*',          'Access-Control-Allow-Credentials': 'true'        }      }      // и возвращаем его      return response    }    // Добавляем данные пользователя в таблицу в виде новой строки    await sheet.addRow(data)    // Формируем ответ    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  }}

Бессерверные функции имеют такую сигнатуру:


exports.handler = (event, context, callback) => {...}

Если очень коротко, то event, как было отмечено ранее, это объект запроса (данные, поступающие от клиента), context любая дополнительная информация, связанная с запросом, например, статус пользователя, callback нужен в случае синхронной (блокирующей) функции для возврата ответа (мы используем асинхронную функцию, поэтому нам данный аргумент не нужен, впрочем, как и аргумент context).


Что касается этих заголовков ответа:


headers: {  'Access-Control-Allow-Origin': '*',  'Access-Control-Allow-Credentials': 'true'}

То они связаны с внутренними настройками Netlify (с выполняемыми перенаправлениями при обращении к функции из клиента). Перенаправления блокируются CORS (Cross-Origin Resource Sharing доступ к ресурсу из другого источника), потому что бессерверные функции не совсем бессерверные, под капотом они работают на основе централизованного сервера. Эти заголовки не требуются для локальной разработки, но развернуть приложение на хостинге без них не получится. В официальной документации про это ни слова. Возможно, к тому моменту, когда вы будете читать данную статью, этот недостаток будет устранен.


Следует отметить, что эти заголовки можно указать для всех ответов в файле netlify.toml.


Итак, мы реализовали функцию для подписки на обновления. Вторую функцию мы реализуем позже, когда выясним, какие данные нам для этого нужны, как нам получить эти данные на клиенте и передать их в функцию.


Клиент


Устанавливаем зависимости для клиента:


yarn add react-router-dom semantic-ui-css semantic-ui-react react-google-recaptcha# илиnpm i ...

Код клиента находится в директории src. Удаляем из нее лишние файлы (оставляем только index.js и index.css), создаем директорию pages для страниц и hooks для пользовательских хуков. В директории pages создаем следующие файлы:


  • Home.js домашняя/главная страница
  • Subscribe.js страница с формой
  • Success.js страница с сообщением об успехе операции
  • NotFound.js резервная страница (ошибка 404)

В директории hooks создаем три файла:


  • useDeferredRoute.js хук для отложенной маршрутизации (опционально)
  • useTimeout.js хук-обертка для setTimeout
  • index.js экспорт индикатора загрузки и ре-экспорт хуков

Структура директории src:


src  hooks    index.js    useDeferredRoute.js    useTimeout.js  pages    Home.js    NotFound.js    Subscribe.js    Success.js  index.css  index.js

В index.css мы подключаем кастомный шрифт и вносим небольшие правки в стили semantic-ui:


@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');* {  font-family: 'Montserrat', sans-serif !important;}body {  min-height: 100vh;  display: grid;  align-content: center;  background: #8360c3;  background: linear-gradient(135deg, #2ebf91, #8360c3);}h2 {  margin-bottom: 3rem;}.ui.container {  max-width: 480px !important;  margin: 0 auto !important;  text-align: center;}.ui.form {  max-width: 300px;  margin: 0 auto;}.ui.form .field > label {  text-align: left;  font-size: 1.2rem;  margin-bottom: 0.8rem;}.ui.button {  margin-top: 1.5rem;  font-size: 1rem;  letter-spacing: 1px;  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;}.email-error {  color: #f93154;  text-align: left;}

В index.js мы импортируем компоненты приложения и реализуем разделение кода на уровне маршрутов с помощью lazy и Suspense:


import React, { lazy, Suspense } from 'react'import ReactDOM from 'react-dom'// Средства для маршрутизацииimport { BrowserRouter as Router, Switch, Route } from 'react-router-dom'// Индикатор загрузкиimport { Spinner } from './hooks'// Стили `semantic-ui`import 'semantic-ui-css/semantic.min.css'// Кастомные стилиimport './index.css'// "Ленивые" компоненты - динамический импортconst Home = lazy(() => import('./pages/Home'))const Subscribe = lazy(() => import('./pages/Subscribe'))const Success = lazy(() => import('./pages/Success'))const NotFound = lazy(() => import('./pages/NotFound'))ReactDOM.render(  <Suspense fallback={<Spinner />}>    <Router>      <Switch>        <Route path='/' exact component={Home} />        <Route path='/subscribe' component={Subscribe} />        <Route path='/success' component={Success} />        <Route component={NotFound} />      </Switch>    </Router>  </Suspense>,  document.getElementById('root'))

Рассмотрим, что из себя представляют пользовательские хуки.


Хук useDeferredRoute добавляет искусственную задержку при начальной загрузке приложения и переходе пользователя к другой странице. Это совершенно не обязательно, возможно, кто-то даже скажет, что это плохая практика, но мне показалось, что так приложение будет более "живым". Кроме того, не так давно при работе над одним из проектов я столкнулся с необходимостью скрытия от пользователя позиционирования элементов фона отрендеренного компонента через искусственную задержку отображения компонента (вот где бы пригодился хук useTransition, но он пока еще экспериментальный).


import { useState, useEffect } from 'react'const sleep = (ms) => new Promise((r) => setTimeout(r, ms))export const useDeferredRoute = (ms) => {  const [loading, setLoading] = useState(true)  useEffect(() => {    const wait = async () => {      await sleep(ms)      setLoading(false)    }    wait()  }, [ms])  return { loading }}

Хук useTimeout, как было отмечено, это всего лишь обертка над нативным setTimeout:


import { useEffect, useRef } from 'react'export const useTimeout = (cb, ms) => {  const cbRef = useRef()  useEffect(() => {    cbRef.current = cb  }, [cb])  useEffect(() => {    function tick() {      cbRef.current()    }    if (ms > 1) {      const id = setTimeout(tick, ms)      return () => {        clearTimeout(id)      }    }  }, [ms])}

А вот как выглядит hooks/index.js:


// Мне не хотелось создавать директорию `components` для одного компонентаimport { Loader } from 'semantic-ui-react'export const Spinner = () => <Loader active inverted size='large' />export { useDeferredRoute } from './useDeferredRoute'export { useTimeout } from './useTimeout'

Теперь займемся страницами.


В Home.js нет ничего особенного. После скрытия индикатора загрузки, мы приветствуем пользователя и предлагаем ему подписаться на (кнопка "Подписаться" это на самом деле ссылка на страницу Subscribe):


import { Link } from 'react-router-dom'import { Container, Button } from 'semantic-ui-react'import { Spinner, useDeferredRoute } from '../hooks'function Home() {  const { loading } = useDeferredRoute(1500)  if (loading) return <Spinner />  return (    <Container>      <h2>Доброго времени суток!</h2>      <h3>        Подпишитесь на обновления, <br /> чтобы оставаться в курсе событий!      </h3>      <Button color='teal' as={Link} to='/subscribe'>        Подписаться      </Button>    </Container>  )}export default Home

В Success.js также нет ничего особенного. После скрытия индикатора загрузки, мы благодарим пользователя за подписку и выполняем автоматическое перенаправление на главную страницу через 3 секунды с помощью нашего хука useTimeout. На случай, если автоматического перенаправления не произошло, имеется кнопка-ссылка на страницу Home:


import { Link, useHistory } from 'react-router-dom'import { Container, Button } from 'semantic-ui-react'import { Spinner, useDeferredRoute, useTimeout } from '../hooks'function Success() {  const { loading } = useDeferredRoute(500)  const history = useHistory()  const redirectToHomePage = () => {    history.push('/')  }  useTimeout(redirectToHomePage, 3000)  if (loading) return <Spinner />  return (    <Container>      <h2>Спасибо за подписку!</h2>      <h3>Сейчас вы будете перенаправлены на главную страницу</h3>      <Button color='teal' as={Link} to='/'>        На главную      </Button>    </Container>  )}export default Success

Еще одна простая страница NotFound пользователь попадает на эту страницу при отсутствии совпадения с маршрутами приложения:


import { Link, useHistory } from 'react-router-dom'import { Container, Button } from 'semantic-ui-react'import { Spinner, useDeferredRoute, useTimeout } from '../hooks'function NotFound() {  const { loading } = useDeferredRoute(500)  const history = useHistory()  const redirectToHomePage = () => {    history.push('/')  }  useTimeout(redirectToHomePage, 2000)  if (loading) return <Spinner />  return (    <Container>      <h2>Страница отсутствует</h2>      <h3>Сейчас вы будете перенаправлены на главную страницу</h3>      <Button color='teal' as={Link} to='/'>        На главную      </Button>    </Container>  )}export default NotFound

На странице Subscribe используется компонент react-google-recaptcha, которому в качестве пропа передается ключ сайта (sitekey). Данный ключ можно получить в административной консоли Google ReCAPTCHA, но для этого приложение надо сначала развернуть на Netlify. К счастью, для локальной разработки можно использовать этот тестовый ключ (это официальный ключ для тестирования): 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI. Позже мы вернемся к этому вопросу.


Еще один важный момент это конечная точка отправки пользовательских данных. Она должна начинаться с /.netlify/, затем указывается путь к соответствующей функции: functions/subscribe /.netlify/functions/subscribe (название функции часть пути). Следует отметить, что часть пути /.netlify/functions можно изменить в netlify.toml.


В остальном, все просто: после скрытия индикатора загрузки, мы предлагаем пользователю ввести свое имя и адрес электронной почты, а также подтвердить, что он "не робот". После заполнения всех полей и прохождения проверки с кнопки "Подписаться" снимается блокировка. При нажатии этой кнопки данные пользователя отправляются в функцию, которая записывает их в таблицу.


import { useState } from 'react'import { useHistory } from 'react-router-dom'import { Container, Form, Button } from 'semantic-ui-react'import ReCAPTCHA from 'react-google-recaptcha'import { Spinner, useDeferredRoute } from '../hooks'// Утилита для проверки того, что все поля заполненыconst isEmpty = (fields) => fields.some((f) => f.trim() === '')// Простой вариант утилиты для проверки адреса электронной почтыconst isEmail = (v) => /\w+@\w+\.\w+/i.test(v)function Subscribe() {  const [formData, setFormData] = useState({    username: '',    email: ''  })  const [error, setError] = useState(null)  const [recaptcha, setRecaptcha] = useState(false)  const { loading } = useDeferredRoute(1000)  const history = useHistory()  const onChange = ({ target: { name, value } }) => {    setError(null)    setFormData({      ...formData,      [name]: value    })  }  const onSubmit = async (e) => {    e.preventDefault()    const email = isEmail(formData.email)    if (!email) {      return setError('Введен неправильный email')    }    try {      const response = await fetch('/.netlify/functions/subscribe', {        method: 'POST',        body: JSON.stringify(formData),        headers: {          'Content-Type': 'application/json'        }      })      if (!response.ok) {        const json = await response.json()        return setError(json.error)      }      history.push('/success')    } catch (err) {      console.error(err)    }  }  // Учитывая, что мы используем тестовый ключ, капча всегда будет иметь истинное значение  const disabled = isEmpty(Object.values(formData)) || !recaptcha  const { username, email } = formData  if (loading) return <Spinner />  return (    <Container>      <h2>Подписаться на уведомления</h2>      <Form onSubmit={onSubmit}>        <Form.Field>          <label>Ваше имя</label>          <input            placeholder='Имя'            type='text'            name='username'            value={username}            onChange={onChange}            required          />        </Form.Field>        <Form.Field>          <label>Ваш email</label>          <input            placeholder='Email'            type='email'            name='email'            value={email}            onChange={onChange}            required          />        </Form.Field>        <p className='email-error'>{error}</p>        <ReCAPTCHA          sitekey='6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'          onChange={() => setRecaptcha(true)}        />        <Button color='teal' type='submit' disabled={disabled}>          Подписаться        </Button>      </Form>    </Container>  )}export default Subscribe

Итак, у нас имеется клиентская часть приложения и функция для записи данных пользователя в таблицу. Пришло время проверить работоспособность приложения.


Если вы еще не установили netlify-cli, самое время это сделать:


yarn global add netlify-cli# илиnpm i -g ...

Все, что нужно сделать для запуска приложения и инициализации функции, это выполнить следующую команду:


netlify dev

После выполнения указанной команды клиент будет запущен по адресу localhost:3000, а сервер также на локальном хосте, но с портом 8888.


Прелесть в том, что netlify-cli умеет автоматически определять, какой фреймворк используется в проекте, и выполнять нужные команды для его запуска.


Нажимаем на кнопку "Подписаться", заполняем поля формы, проходим проверку и снова нажимаем на "Подписаться". Выполняется перенаправление на страницу с сообщением об успехе операции, затем еще одно перенаправление на главную страницу. Открываем таблицу, видим введенные нами данные.


.


.


.


Отлично, приложение работает, как ожидается.


В следующей части туториала мы развернем приложение на Netlify, закончим настройку капчи, реализуем автоматическую рассылку уведомлений и отписку от обновлений.


Благодарю за внимание и хорошего дня!




Облачные серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Реализация подписки на обновления с помощью Google Sheets, Netlify Functions и React. Часть 2

08.06.2021 12:19:01 | Автор: admin

Это вторая часть туториала, посвященного реализации 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).




Не будем ходить вокруг да около, а сразу развернем приложение в продакшн-режиме:


netlify deploy --prod

Готово. Легко, правда? Вот за что я люблю Netlify.


Теперь, когда у нас имеется URL, мы можем зарегистрировать наше приложение в Google ReCAPTCHA.


Заходим в консоль администратора и создаем новое приложение (+). Вводим название сайта (ярлык), выбираем reCAPTCHA v2, указываем домен (URL нашего приложения без протокола), принимаем условия использования (флажок "Отправлять владельцам оповещения" можно снять), нажимаем "Отправить". Получаем ключ сайта и секретный ключ, нам нужен только первый.





Добавляем в .env такую переменную:


REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY=YOUR_SITE_KEY

Вносим изменение в Subscribe.js:


<ReCAPTCHAsitekey={process.env.REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY}onChange={() => setRecaptcha(true)}/>

Повторно собираем и разворачиваем приложение:


yarn build# илиnpm run build# иnetlify deploy --prod

Если все сделано правильно, то на странице с формой появится настоящая капча.



Отлично.


Переходим к автоматизации рассылки уведомлений и реализации возможности отписаться от обновлений.


Автоматическая рассылка уведомлений


Давайте подумаем о том, что нам нужно для того, чтобы пользователь имел возможность отписаться от обновлений.


Мы, например, могли бы добавлять в уведомление специальную ссылку, при переходе по которой выполняется отписка. Для идентификации пользователя, выразившего такое желание, нам нужен его адрес электронной почты. Следовательно, ссылка должна формироваться из пути к соответствующей странице нашего приложения + email пользователя (для того, чтобы мы могли извлекать его из параметров строки запроса).


Для реализации автоматической рассылки уведомлений мы будем использовать nodemailer, а для тестирования Mailtrap.


Создаем аккаунт на Mailtrap, открываем автоматически созданный проект MyInbox, на вкладке SMTP Settings в разделе Integrations выбираем Node.js -> Nodemailer, получаем данные для авторизации.




Сохраняем эти данные в .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="http://personeltest.ru/aways/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  резервный контент на случай, если почтовый клиент пользователя не поддерживаем сообщения в формате HTMLrows.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.



Для взаимодействия с реальными почтовыми службами (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


Существует один нюанс, связанный с использованием Gmail в качестве сервиса для рассылки писем гугл может блокировать к нему доступ, считая приложение ненадежным (существует платная версия Gmail Google Workspace, которая с точки зрения гугла, конечно же, является надежной).


Для того, чтобы снять блокировку, заходим в настройки гугл аккаунта, открываем вкладку Безопасность, переходим в раздел Ненадежные приложения, у которых есть доступ к аккаунту и разрешаем небезопасные приложения.



С реализацией автоматической рассылки мы закончили, но при переходе по ссылке, указанной в письме, мы попадаем на резервную страницу, поскольку в нашем приложении нет страницы для отписки от обновлений, как и соответствующей бессерверной функции. Давайте это исправим.


Отписка от обновлений


Добавляем в приложение (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 не найден.




Туториал получился гораздо больше, чем я изначально планировал, но, надеюсь, вы не жалеете потраченного времени. Я постарался максимально подробно осветить все аспекты, связанные с разработкой нашего приложения, прошу меня извинить, если что-то упустил.


Как бы то ни было, если вы в точности следовали инструкциям, а еще лучше реализовали какие-то дополнительные возможности, то в вашем портфолио появилось настоящее Real World App, разработанное с использованием самых современных технологий (да, мы не использовали TypeScript, но для нашего небольшого проекта это было бы слишком круто).




Облачные серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru