Node JS и React - как создать фулстек приложение. Полное руководство

2 года назад·3 мин. на чтение

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

Необходимые инструменты

Перед началом нужно убедиться, что на компьютере установлены все необходимые библиотеки, IDE и ПО, а именно:
  • NodeJS и npm. Их можно скачать с официального сайта nodejs.org. npm установится автоматически вместе с NodeJS.
  • Предпочитаемый IDE, например, Visual Studio Code.
  • Опционально, установить git для удобной работы с кодом.

О приложении

В этой статье напишем приложение, которое будет получать и отображать список дел. Структура папок будет выглядеть следующим образом.
app/
  frontend/
  backend/

Создание бэкэнда на NodeJS

Запустим команду в папке app/backend для инициализации проекта:
npm init -y
Эта команда создаст файл package.json. Этот файл содержит как общую информацию о проекте (название, версия, описание и т.д.), так и информацию о зависимостях, скрипты для запуска, сборки и тестирования. Для создания сервера будем использовать express. Установим его с помощью команды:
npm i express
Создадим файл index.js, который будет содержать код для запуска сервера. Этот код запускает веб сервер на порту 3010, если он не задан в переменных среды.
// backend/index.js
const express = require('express');
 
const PORT = process.env.PORT || 3010;
const app = express();
 
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  next();
});
 
app.listen(PORT, () => {
  console.log(`Server listening on ${PORT}`);
});
Добавим команду для запуска сервера в package.json. В результате сможем запускать наш сервер с помощью команды npm start.
// backend/package.json
...
"scripts": {
  "start": "node ./index.js"
},
...
Из директории app/backend запустим команду npm start. Если ошибок нет, получим сообщение, что сервер прослушивает порт 3010.
PS C:\tutorials-coding\nodejs-react-app\backend> npm start        

> backend@1.0.0 start
> node ./index.js

Server listening on 3010

Создание API

API это интерфейс, с помощью которого React приложение будет общаться с веб-сервером, т.е. запрашивать, изменять или удалять данные. В нашем случае мы создадим API для получения списка дел в формате JSON. Создадим файл todo-items.json c объектами todo. Этот массив будем отдавать по запросу /api/todo-items.
[
  {
    "id": 1,
    "text": "Изучить NodeJS",
    "done": true
  },
  {
    "id": 2,
    "text": "Изучить ReactJS",
    "done": true
  },
  {
    "id": 3,
    "text": "Написать приложение",
    "done": false
  }
]
Следующий код создает эндпоинт /api/todo-items. React приложение будет отправлять GET запрос на этот эндпоинт.
// backend/index.js

// ...

const todoItems = require('./todo-items.json');
app.get('/api/todo-items', (req, res) => {
  res.json({ data: todoItems });
});

app.listen(PORT, () => {
  console.log(`Server listening on ${PORT}`);
});
Для того чтобы изменения вступили в силу, нужно перезапустить NodeJS сервер. Для остановки скрипта - в терминале, в котором запущен npm start, нужно нажать Ctrl + C (Command + C). Далее снова запускаем npm start. Для проверки эндпоинта, в браузере перейдем по адресу http://localhost:3010/api/todo-items. В результате получим, такой ответ. Ответ от Node JS сервера

Создание фронтенда на React

В папке app/ откроем новый терминал и запустим команду для создания React приложения, где frontend имя нашего приложения.
npx create-react-app@latest frontend
Дождемся установки всех зависимостей. В терминале перейдем в папку frontend.
cd ./frontend
Установим библиотеку bootstrap для дальнейшего использования готовых компонентов.
npm install react-bootstrap bootstrap
Заимпортируем bootstrap.min.css в файле frontend/src/index.js.
import 'bootstrap/dist/css/bootstrap.min.css';
Запустим приложение командой npm start.
npm start
Получим следующее сообщение. Перейдем по указанному адресу в браузере.
Compiled successfully!
 
You can now view frontend in the browser.        
 
  Local:            http://localhost:3003        
  On Your Network:  http://192.168.99.1:3003     
 
Note that the development build is not optimized.
To create a production build, use npm run build. 

Отправка HTTP запроса из React в NodeJS

К этому моменту у нас уже есть рабочий сервер, который умеет принимать запросы и отдавать данные. Сделаем запрос на /api/todo-items из React приложения. Для этого вызовем функцию fetch из хука useEffect в файле App.js.
// frontend/src/App.js
import { useState, useEffect } from 'react';
import Form from 'react-bootstrap/Form';
import './App.css';
 
function App() {
  const [todoItems, setTodoItems] = useState([]);
 
  useEffect(() => {
    fetch('http://localhost:3010/api/todo-items')
      .then((res) => res.json())
      .then((result) => setTodoItems(result.data));
  }, []);
 
  return (
    <div>
      {todoItems.map((item) => (
        <Form.Group key={item.id} className="app__todo-item">
          <Form.Check type="checkbox" checked={item.done} />
          <Form.Control type="text" value={item.text} />
        </Form.Group>
      ))}
    </div>
  );
}
 
export default App;
Открыв приложение в браузере, получим такой результат. React приложение со списком дел Исходный код

Композиция функций. Функциональное программирование

2 года назад·5 мин. на чтение

Композиция — это способ построения больших модулей из более мелких. В этой статье подробно рассмотрим композицию на примере JavaScript.

Это серия статей о функциональном программировании:
  1. Парадигмы программирования
  2. Композиция (рассматривается в этой статье)
  3. Функторы
  4. Каррирование
  5. Чистые функции
  6. Функции первого класса

Что такое композиция функций?

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

Как работает композиция?

Математическая запись определения композиции выглядит следующим образом:
(f ∘ g)(x) = f(g(x))
В JavaScript композиция, где f и g - это функции, будет выглядеть так:
const compose = (f, g) => (x) => f(g(x))
Функция compose является композицией функций f и g. Выходные данные функции g будут переданы на вход функции f. По другому эта запись выглядела бы следующим образом:
const compose = (f, g) => {
  return (x) => {
    const gResult = g(x)
    const fResult = f(gResult)
    return fResult
  }
}

Пример работы функции compose

Рассмотрим пример с более конкретными функциями:
const getAge = (user) => user.age
const isAgeAllowed = (age) => age >= 30
Теперь составим композицию из этих маленьких функций - getAge и isAgeAllowed:
const user = { name: 'John', age: 35 }
const isAllowedUser = compose(
 isAgeAllowed,
 getAge
)
isAllowedUser(user) // true
compose выполняет функции справа налево. Мы отправляем объект user в функцию isAgeAllowed. Далее user попадает сначала в getAge, потом результат этой функции попадает в isAgeAllowed.

compose и pipe

Функция pipe очень похожа на функцию compose. Они выполняют одну и ту же роль. Обе объединяют функции в цепочки. Однако их реализация и порядок выполнения функций отличается.

Реализация compose и pipe

compose реализован следующим образом:
const compose = (...fns) => {
  return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
}
pipe реализован следующим образом:
const pipe = (...fns) => {
   return (x) => fns.reduce((acc, fn) => fn(acc), x)
}
Они отличаются лишь в применении функции reduce и reduceRight. Это влияет лишь на порядок выполнения функций.

Порядок выполнения функций

compose выполняет функции справа налево. pipe выполняет функции слева направо.

Пример

Предположим имеется 3 функции: f, g и h. При использовании compose:
compose(f, g, h)
        ←  ←  ←
Порядок выполнения функций будет таким h, g и далее f (справа налево). И выходные данные будут передаваться в следующую функцию. При использовании pipe:
pipe(f, g, h)
     →  →  →
Порядок выполнения функций будет таким f, g и далее h (слева направо). И выходные данные будут передаваться в следующую функцию.

Какую функцию использовать?

compose и pipe не сильно отличаются друг от друга. Они решают одну и ту же задачу. compose ближе к математической нотации (f ∘ g)(x) = f(g(x)). С pipe визуально легче воспринимать порядок выполнения функций.

Реальный пример

В следующих примерах будем использовать pipe. Предположим, нужно сделать простой калькулятор цен, в котором нужно применить:
  • налог (30%, по умолчанию),
  • сервисный сбор (10у.е., по умолчанию),
  • скидку,
  • купон,
  • цену доставки на основе веса.
API калькулятора цен на основе этих требований будет выглядеть следующим образом:
const priceCalculator = (
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
) => {
  // реализация
}

Реализация без композиции

const priceCalculator = (
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
) => {
  return (
    price
     - (price * percentCoupon)
     - discount
     + (weight * shippingPricePerKg)
     + serviceFees
  ) * (1 + taxPercentage)
}
Эта функция выполняет свою задачу, но ее сложно читать, тестировать и отлаживать.

Реализация с композицией

const priceCalculator = (
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
) => {
 const applyTax = (val) => val * (1 + taxPercentage)
 const applyServiceFees = (val) => val + serviceFees
 const applyPercentCoupon = (val) => val - val * percentCoupon
 const applyDiscount = (val) => val - discount
 const applyShippingCost = (val) => val + weight * shippingPricePerKg

 return pipe(
   applyPercentCoupon,
   applyDiscount,
   applyShippingCost,
   applyServiceFees,
   applyTax
 )(price)
}
Эта реализация выглядит более чистой, ее легко тестировать и отлаживать. Все это благодаря модульности.

Запуск примера

Запустим, передав только price. Очевидно, получим неверный результат, т.к. передали не все обязательные параметры.
priceCalculator(10) // NaN
Сначала продебажим. Для дебага pipe и compose можно добавить удобную функцию inspect. Она просто логирует в консоль входные данные и возвращает эти данные без изменений.
const inspect = (label) => (x) => {
  console.log(`${label}: ${x}`)
  return x
}
Добавим inspect в цепочку выполнения.
const priceCalculator = (
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
) => {
  const applyTax = (val) => val * (1 + taxPercentage)
  const applyServiceFees = (val) => val + serviceFees
  const applyPercentCoupon = (val) => val - val * percentCoupon
  const applyDiscount = (val) => val - discount
  const applyShippingCost = (val) => val + weight * shippingPricePerKg

  return pipe(
    inspect('price'),
    applyPercentCoupon,
    inspect('after applyPercentCoupon'),
    applyDiscount,
    inspect('after applyDiscount'),
    applyShippingCost,
    inspect('after applyShippingCost'),
    applyServiceFees,
    inspect('after applyServiceFees'),
    applyTax
  )(price)
}
Результат будет примерно таким:
priceCalculator(10)
// price: undefined
// after applyPercentCoupon: NaN
// ...
Видим, price - undefined. Это произошло потому что price - третий аргумент, а мы передаем его первым.

Быстрый фикс

Сделаем так, чтобы функция принимала один объект:
const priceCalculator = ({
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
}) => {
  // ...
}
Далее, используем:
priceCalculator({ price: 10 })
 
// price: 10
// after applyPercentCoupon: NaN
// ...
Все еще получаем NaN, в этот раз потому что не был передан percentCoupon и он имеет значение undefined.
Добавим для всех параметров, кроме price, значения по умолчанию.
const priceCalculator = ({
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount = 0,
  percentCoupon = 0,
  weight = 0,
  shippingPricePerKg = 0
}) => {
  // ...
}
Если запустить заново, получим результат:
priceCalculator({ price: 10 }) // 26
Это пример того как композиция позволяет нам тестировать и исправлять код проще и быстрее, просто проверяя области, которые вызывают подозрения. Проблема, которую мы отлаживали, была очень простой. Когда мы переходим к более масштабным модулям, все становится еще более трудным для проверки. Большие атомарные функции трудно поддерживать. Разделение функций на более мелкие упрощает отладку, тестирование, поддержку и разработку функций.

Для чего нужна композиция?

Композиция это объединение меньших модулей в более крупные. Если мы думаем оперируем модулями (что обеспечивается композицией), мы улучшаем:
  • модульное мышление,
  • тестируемость,
  • возможность отладки,
  • поддерживаемость.

Итоги

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