Как создать полнофункциональное приложение To-Do с нуля с помощью Node.js, express, mongoose, React

1. Предпосылки

Прежде чем приступить к работе с MERN TODO APP, вы должны иметь общее представление о следующих технологиях:

  • Node.js: знайте, как его запускать и понимать его основной синтаксис.
  • Express.js: узнайте, как его установить, и основы создания маршрутов.
  • Mongoose: Понимание основ создания схем и данных моделирования.
  • Базовая реакция: UseState, UseEffect, fetch(), Props, onClick(), onChange()
  • Почтальон: Знайте, как тестировать конечные точки API.

2. Настройте внутренний сервер

Для начала создайте новую папку с именем «api» для вашего внутреннего кода. Внутри этой папки вы настроите сервер и свяжете его с базой данных MongoDB.

В папке «api» откройте терминал и выполните команду `npm init`, чтобы инициализировать новый проект Node.js. Будет создан файл package.json с информацией о проекте и зависимостями.

Далее нам нужно установить необходимые модули Node.js, используя следующую команду.

npm install express cors mongoose dotenv 

Файл package.json теперь будет содержать зависимости: cors, dotenv, express, mongoose.

Затем настройте файл server.js, потребовав необходимые модули и настроив приложение Express.

  • файл server.js
const express = require('express'); 
const cors = require('cors'); 
const mongoose = require('mongoose'); 
require('dotenv').config(); 

//Execute express 
const app = express(); 

//Middlewares
app.use(express.json()); 
app.use(cors()); 

const port = 4001; 

app.listen(port, () => console.log(`Server is running on port ${port}`)); 

Теперь, если вы вводите запятую «node server.js», вы увидите сообщение «Сервер работает на порту 4001».

3. Подключиться к базе данных

*Убедитесь, что у вас установлен MongoDB Atlas, и подключитесь к кластеру!*

Перед подключением к базе данных MongoDB необходимо создать файл .env в корне вашего проекта. В этом файле сохраните URI подключения MongoDB следующим образом:

В файле .env добавьте URI подключения к MongoDB.

//.env file 
MONGO_URI= mongodb+srv://${username}:${password}@${cluster}.mongodb.net/${dbname}?retryWrites=true&w=majority
 

Обязательно замените «имя пользователя», «пароль», «кластер» и «имя базы данных» своими учетными данными MongoDB.

Затем добавьте файл .env в свой файл .gitignore. Это гарантирует, что ваш URI MongoDB останется закрытым и не будет отправлен в репозиторий GitHub.

В файле server.js получите доступ к MONGO_URI из файла .env и подключите Mongoose к базе данных:

const connectionString = process.env.MONGO_URI; 
mongoose.connect(connectionString)
        .then(() => console.log('Connected to the database…')) 
        .catch((err) => console.error('Connection error:', err));

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

4. Определите схему базы данных

Создайте папку «models» и создайте новый файл с именем «Todo.js», чтобы определить схему базы данных для наших элементов TODO:

  • Файл Todo.js
//Require mongoose
const mongoose = require('mongoose'); 

//Create schema contains a single field named 'name.' 
//The 'name' field is of type String
const TodoSchema = new mongoose.Schema({ name: String }); 

//Export the Mongoose model with the collection name "Todo"
module.exports = mongoose.model('Todo', TodoSchema);

5. Создание маршрутов API

Теперь пришло время создать маршруты API для взаимодействия с базой данных. В файле server.js импортируйте схему Todo:

const Todo = require(‘./models/Todo’);

После импорта схемы Todo мы можем приступить к созданию маршрутов. Мы реализуем запросы GET, POST и DELETE, что позволит пользователям просматривать все задачи, создавать новые задачи и удалять существующие задачи. Эти задачи будут сохранены в базе данных.

5-1.Запрос GET: получить все задачи

app.get('/todo', async (req, res) => { 
const todos = await Todo.find(); 
res.json(todos); });

Сначала мы обрабатываем запрос GET, отправленный на конечную точку «/todo». Когда клиент делает этот запрос, наш сервер извлекает все задачи из модели Todo, используя метод Todo.find(). Затем полученные задачи будут преобразованы в формат JSON и отправлены обратно клиенту в качестве ответа.

5–2. POST-запрос: создать новую задачу

app.post('/todo/new', async (req,res) => {
    const newTask = await Todo.create(req.body);
    res.status(201).json({newTask})
})

Запрос POST отправлен на конечную точку «/todo/new». Когда пользователь создает новую задачу, запрос будет содержать необходимые данные в теле запроса. Мы используем метод Todo.create() для создания новой задачи в модели Todo, используя данные, полученные из тела запроса.

После успешного создания новой задачи мы отвечаем кодом состояния 201 (указывающим на успешное создание) и отправляем сведения о вновь созданной задаче в формате JSON.

Примечание. Использование отдельного маршрута «/todo/new» для создания новой задачи является хорошей практикой для организации API и соблюдения соглашений RESTful.

5–3. УДАЛИТЬ Запрос: удалить задачу

app.delete('/todo/delete/:id', async(req,res)=>{
    const result = await Todo.findByIdAndDelete(req.params.id)
    res.json(result)
})

В этом последнем разделе мы обрабатываем запрос DELETE, отправленный на конечную точку ‘/todo/delete/:id’. ‘:id’ — это параметр маршрута, представляющий уникальный идентификатор удаляемой задачи. Когда сервер получает запрос DELETE с идентификатором конкретной задачи, он использует метод Todo.findByIdAndDelete() для поиска и удаления соответствующей задачи из модели Todo.

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

Отличная работа! Теперь наш код выглядит так!

  • файл server.js
const express = require('express'); 
const cors = require('cors'); 
const mongoose = require('mongoose'); 
require('dotenv').config(); 
const Todo = require('./models/Todo');


const app = express(); 

app.use(express.json()); 
app.use(cors()); 

const port = 4001; 

const connectionString = process.env.MONGO_URI; 
mongoose.connect(connectionString)
.then(() => console.log('Connected to the database…'))
.catch((err) => console.error('Connection error:', err));

//Routes 
app.get('/todo', async (req, res) => { 
   const allTasks = await Todo.find();
   res.json(allTasks)
 });

app.post('/todo/new', async (req,res) => {
    const newTask = await Todo.create(req.body);
    res.status(201).json({newTask})
})

app.delete('/todo/delete/:id', async(req,res)=>{
    const result = await Todo.findByIdAndDelete(req.params.id)
    res.json(result)
})


app.listen(port, () => console.log(`Server is running on port ${port}`));

6. Протестируйте конечные точки API на POSTMAN

Давайте проверим наши конечные точки API на Postman, чтобы убедиться, что все работает правильно!

Проверка конечных точек с помощью Postman — самый простой способ протестировать API. Мы можем легко отправлять новые данные в наш API и быстро проверять результаты.

Во-первых, давайте проверим запрос POST. Как вы можете видеть ниже, выберите метод POST и используйте конечную точку: http://localhost:4001/todo/new. В теле выберите сырой, выберите формат JSON и создайте новую фиктивную задачу. Вы можете увидеть результат ниже; мы успешно получили наш запрос.

Теперь давайте проверим запрос GET. Как видите, теперь у нас есть новая задача, которую мы только что создали.

Наконец, давайте проверим запрос DELETE. Мы успешно удалили созданный нами элемент!

7. Реагируйте на HTML

Давайте установим React в вашу основную папку.

Выполните следующую команду:

npx create-react-app todo

Теперь у вас будет две папки. «api» и «todo» внутри основной папки. Внутри папки «todo» мы создадим приложение React.

Далее мы изменим шаблонный код React в соответствии с нашими потребностями.

  • папка todo — файл index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
  • папка с делами — файл App.js
import { useEffect, useState } from "react";
import TodoItem from "./TodoItem";

function App() {

  return (
    <div className="container">
      <div className="heading">
        <h1>TO-DO-APP</h1>
      </div>

      <div className="form">
        <input type='text'></input>
        <button>
          <span>ADD</span>
        </button>
      </div>

      <div className="todolist">  
        <TodoItem/>  
      </div>
    </div>
 
  );
}

export default App;

Это структура HTML для нашего приложения React. У нас есть основной контейнер, в котором хранятся все компоненты. Внутри основного контейнера у нас есть поле ввода и кнопка добавления для создания новых элементов списка дел. Кроме того, у нас есть контейнер списка дел для отображения элементов списка дел. Чтобы сделать код более организованным и управляемым, мы выделим компонент TodoItem в отдельный файл. Это поможет сохранить кодовую базу читабельной и ремонтопригодной.

  • папка с делами — файл TodoItem.js
import React, {useState} from "react";

function TodoItem(){
    return(
     <div className="todo">
        <div className="text">Test</div>
        <div className="delete-todo"><span >X</span></div>
      </div>
    )
}

export default TodoItem;

Это компонент TodoItem, который представляет отдельный элемент списка дел в контейнере списка дел.

Внутри TodoItem у нас есть следующие элементы:

  1. text div: в этом div отображается название задачи (в данном примере отображается «Тест»).
  2. delete-todo div: Этот div содержит значок «X», который представляет собой кнопку удаления для удаления элемента списка дел.

8. Реагируйте на стиль

  • файл index.css
:root {
 --primary: #D81E5B;
 --light: #EEE;
 --light-alt: #61759b;
 --dark: #131A26;
}

* {
 margin: 0;
 padding: 0;
 box-sizing: border-box;
 font-family: "Fira Sans", sans-serif;
}

body {
 background-color: var(--light-alt);
 color: var(--light);
}

.container {
  max-width: 20rem;
  padding: 32px;
  margin: 0 auto;
}

.heading{
  text-align: center;
}

.form{
  margin-top: 2rem;
}

.form input {
  width: 85%;
  padding: 3px;
}

.form button {
  padding: 3px;
  border: none;
  cursor: pointer;
}

.todo {
  padding: 10px;
  border-radius: 13px;
  background-color: var(--dark);
  margin-top: 2rem;
  display: grid;
  grid-template-columns: 3fr 0.3fr;
  transition: 0.5s;
  cursor: pointer;
}

.checkbox {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background-color: var(--light);
  transition: 0.4s;
}

.check-complete .checkbox{
  background-color: var(--primary); 
}

.check-complete .text{
  text-decoration: line-through;
}


.text {
  margin-left: 1rem;
}

.delete-todo {
  display: flex;
  justify-content: center;
  align-items: center; 
}

После настройки наш результат выглядит так:

9. ПОЛУЧИТЕ все задания

Во-первых, мы получим все задачи из нашей базы данных через наш API.

import { useEffect, useState } from "react";
import TodoItem from "./TodoItem";

//Add API base
const API_BASE= 'http://localhost:4001/todo';

function App() {
  
 //Add useState, we ll store items in the array
  const [items, setItems] = useState([]);

 //Add useEffect, GetTodos() will run every time the component renders
 useEffect(() => {
    GetTodos();
  }, []);

// Add GetTodos() function, fetches data from our API, converts to JSON
// and then saves the data in the 'items' state
// If there's an error, it will be logged to the console
 const GetTodos = () => {
  fetch(API_BASE)
  .then(res => res.json())
  .then(data => setItems(data))
  .catch(err => console.log(err))
 }

  return (
    <div className="container">
      <div className="heading">
        <h1>TO-DO-APP</h1>
      </div>

      <div className="form">
        <input type='text'></input>
        <button>
          <span>ADD</span>
        </button>
      </div>

      <div className="todolist">  
        <TodoItem/>  
      </div>
    </div>
 
  );
}

export default App;

Теперь вы успешно получили данные из нашей базы данных, извлекая их с помощью нашего API. Отобразим наши данные?

Мы пройдемся по каждому элементу, используя метод карты внутри блока «todolist». Затем мы деконструируем наши ключи, чтобы получить отдельные значения.

Наконец, мы передадим каждое значение компоненту «TodoItem».

  
     <div className="todolist">  
      {items.map((item)=> {
        const {_id, name} = item
        return  <TodoItem name={name} id={_id} setItems={setItems}/>   
      })
      </div>

мы используем метод карты для перебора каждого «элемента» в массиве «элементов». Для каждого «элемента» мы деструктурируем ключи «_id» и «имя» из объекта и сохраняем их в отдельных переменных. Затем мы передаем эти значения «name» и «id» вместе с функцией «setItems» в качестве реквизита для компонента «TodoItem».

Перейдем к файлу TodoItem.js.

import React, {useState} from "react";
//Add API, we ll need it later when we send a delete request
const API_BASE= 'http://localhost:4001/todo';

function TodoItem(props){
 // Pass down props
 // Modify hardcoding content to dynamic content
 // Now it displays the data we created in advance
const {name, id, setItems} = props
    return(
     <div className="todo">
        <div className="text">{name}</div>
        <div className="delete-todo"><span >X</span></div>
      </div>
    )
}

export default TodoItem;

Теперь вы можете видеть данные!

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

10. Создайте новую задачу

Наша цель — создать новую задачу, когда пользователь нажимает кнопку «Добавить». Для этого нам нужно сначала зафиксировать ввод пользователя.

import { useEffect, useState } from "react";
import TodoItem from "./TodoItem";

const API_BASE = 'http://localhost:4001/todo';

function App() {
  
  const [items, setItems] = useState([]);

  // Add input state, we will store the user's input in this state
  const [input, setInput] = useState("");

  useEffect(() => {
    GetTodos();
  }, []);

// Store the target's value into the input state 
  const handleChange = (e) => {
    setInput(e.target.value);
  }

  const GetTodos = () => {
    fetch(API_BASE)
      .then(res => res.json())
      .then(data => setItems(data))
      .catch(err => console.log(err))
  }

  return (
 // The input field's value is now taken from the input state. 
 //It will update dynamically as the user types.
 // Add the onChange method, so that handleChange function will be executed
 // every time something has been changed in the input field.

    <div className="container">
      <div className="heading">
        <h1>TO-DO-APP</h1>
      </div>

      <div className="form">
        <input type='text' value={input} onChange={handleChange}></input>
        <button>
          <span>ADD</span>
        </button>
      </div>

      <div className="todolist">  
        <TodoItem/>  
      </div>
    </div>
  );
}

export default App;

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

Давайте добавим метод onClick к кнопке добавления. Всякий раз, когда кнопка была нажата, будет выполняться функция addItem().

 
  const addItem = async() => {
   const data = await fetch(API_BASE + "/new", {
    method: "POST",
    headers: {
      "content-type" : "application/json"
    },
    body: JSON.stringify({
      name: input,
        })
   }).then(res => res.json()) 
   await GetTodos()
   setInput('')
  }

 <button onClick={()=>addItem()}>
 <span>ADD</span>
 </button>
  1. получить наш API для создания новой задачи

2. Входное значение из состояния будет отправлено в виде объекта с ключом «имя», а его значение будет введено пользователем. Затем преобразуйте в JSON.

3. Ответ от API преобразуется в JSON и сохраняется в переменной данных.

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

5. поле ввода будет очищено после добавления новой задачи. Функция setInput используется для установки состояния поля ввода в пустую строку.

Поздравляем! Теперь вы можете создать новую задачу в приложении React!

11. Удалить задачу

Следующим шагом является удаление элемента, которое следует той же логике, что и создание новой задачи. Мы добавим метод onClick к кнопке X и выполним функцию deleteTodo(). Единственное отличие состоит в том, что мы передадим «id» в качестве аргумента функции deleteTodo(), чтобы она знала, какую задачу удалить.

  • Файл TodoItem.js
  const deleteTodo = async(id) => {
        try{
            const response = await fetch(API_BASE + "/delete/" + id, {
                method: "DELETE",
              });
            if(!response.ok){
                throw new Error("Faild to delete a task")
            } 
            const data = await response.json()
            setItems(items=> items.filter(item=> item._id !== data._id))
        }catch (error) {
            console.error("Error updating task status:", error);
          }
      }

  <div className="delete-todo" onClick={()=>deleteTodo(id)}>
   <span >X</span>
  </div>
  1. Чтобы удалить задачу, нам нужно определить, какую задачу пользователь хочет удалить. Для этого мы используем параметр «id», который однозначно идентифицирует каждую задачу.

2. Когда пользователь нажимает кнопку удаления (кнопка X), выполняется функция «deleteTodo()». Мы передаем «id» задачи в качестве аргумента функции «deleteTodo()».

3. Внутри функции «deleteTodo()» мы делаем запрос к конечной точке удаления нашего API с указанным «id», чтобы удалить задачу с сервера.

4. После успешного удаления задачи на сервере мы хотим обновить наш пользовательский интерфейс, чтобы отразить изменения. Для этого мы используем состояние setItems. Мы фильтруем массив «элементов», оставляя только те задачи, чей «id» не равен «id» задачи, которая была только что удалена.

5. При этом задача, которая была удалена, будет удалена из списка, и пользователь увидит обновленный список задач без удаленной задачи.

Отличная работа! Мы успешно создали полнофункциональное приложение с использованием Mongoose, Express, Node.js и React. Если вы нашли это руководство полезным, я был бы очень признателен, если бы вы могли выразить свою поддержку, хлопнув в ладоши.

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

Спасибо, что присоединились ко мне в этом путешествии по созданию полнофункционального приложения!

* Репозиторий Github: https://github.com/EunbyulNa/MERN-TO-DO-APP