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

Полный стек на примере списка задач (React, Vue, TypeScript, Express, Mongoose)



Доброго времени суток, друзья!

В данном туториале я покажу вам, как создать фуллстек-тудушку.

Наше приложение будет иметь стандартный функционал:

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

Выглядеть наше приложение будет так:


Для более широкого охвата аудитории клиентская часть приложения будет реализована на чистом JavaScript, серверная на Node.js. В качестве абстракции для ноды будет использован Express.js, в качестве базы данных сначала локальное хранилище (Local Storage), затем индексированная база данных (IndexedDB) и, наконец, облачная MongoDB.

При разработке клиентской части будут использованы лучшие практики, предлагаемые такими фреймворками, как React и Vue: разделение кода на автономные переиспользуемые компоненты, повторный рендеринг только тех частей приложения, которые подверглись изменениям и т.д. При этом, необходимый функционал будет реализован настолько просто, насколько это возможно. Мы также воздержимся от смешивания HTML, CSS и JavaScript.

В статье будут приведены примеры реализации клиентской части на React и Vue, а также фуллстек-тудушки на React + TypeScript + Express + Mongoose.

Исходный код всех рассматриваемых в статье проектов находится здесь.

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

Демо нашего приложения:


Итак, поехали.

Клиент


Начнем с клиентской части.

Создаем рабочую директорию, например, javascript-express-mongoose:

mkdir javascript-express-mongoosecd !$code .

Создаем директорию client. В этой директории будет храниться весь клиентский код приложения, за исключением index.html. Создаем следующие папки и файлы:

client  components    Buttons.js    Form.js    Item.js    List.js  src    helpers.js    idb.js    router.js    storage.js  script.js  style.css

В корне проекта создаем index.html следующего содержания:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>JS Todos App</title>    <!-- Подключаем стили -->    <link rel="stylesheet" href="client/style.css" />  </head>  <body>    <div id="root"></div>    <!-- Подключаем скрипт -->    <script src="client/script.js" type="module"></script>  </body></html>

Стили (client/style.css):
@import url('https://fonts.googleapis.com/css2?family=Stylish&display=swap');* {  margin: 0;  padding: 0;  box-sizing: border-box;  font-family: stylish;  font-size: 1rem;  color: #222;}#root {  max-width: 512px;  margin: auto;  text-align: center;}#title {  font-size: 2.25rem;  margin: 0.75rem;}#counter {  font-size: 1.5rem;  margin-bottom: 0.5rem;}#form {  display: flex;  margin-bottom: 0.25rem;}#input {  flex-grow: 1;  border: none;  border-radius: 4px;  box-shadow: 0 0 1px inset #222;  text-align: center;  font-size: 1.15rem;  margin: 0.5rem 0.25rem;}#input:focus {  outline-color: #5bc0de;}.btn {  border: none;  outline: none;  background: #337ab7;  padding: 0.5rem 1rem;  border-radius: 4px;  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);  color: #eee;  margin: 0.5rem 0.25rem;  cursor: pointer;  user-select: none;  width: 102px;  text-shadow: 0 0 1px rgba(0, 0, 0, 0.5);}.btn:active {  box-shadow: 0 0 1px rgba(0, 0, 0, 0.5) inset;}.btn.info {  background: #5bc0de;}.btn.success {  background: #5cb85c;}.btn.warning {  background: #f0ad4e;}.btn.danger {  background: #d9534f;}.btn.filter {  background: none;  color: #222;  text-shadow: none;  border: 1px dashed #222;  box-shadow: none;}.btn.filter.checked {  border: 1px solid #222;}#list {  list-style: none;}.item {  display: flex;  flex-wrap: wrap;  justify-content: space-between;  align-items: center;}.item + .item {  border-top: 1px dashed rgba(0, 0, 0, 0.5);}.text {  flex: 1;  font-size: 1.15rem;  margin: 0.5rem;  padding: 0.5rem;  background: #eee;  border-radius: 4px;}.completed .text {  text-decoration: line-through;  color: #888;}.disabled {  opacity: 0.8;  position: relative;  z-index: -1;}#modal {  position: absolute;  top: 10px;  left: 10px;  padding: 0.5em 1em;  background: rgba(0, 0, 0, 0.5);  border-radius: 4px;  font-size: 1.2em;  color: #eee;}


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


Основные компоненты: 1) форма, включающая поле для ввода текста задачи и кнопку для добавления задачи в список; 2) контейнер с кнопками для фильтрации задач; 3) список задач. Также в качестве основного компонента мы дополнительно выделим элемент списка для обеспечения возможности рендеринга отдельных частей приложения.

Дополнительные элементы: 1) заголовок; 2) счетчик количества невыполненных задач.

Приступаем к созданию компонентов (сверху вниз). Компоненты Form и Buttons являются статическими, а List и Item динамическими. В целях дифференциации статические компоненты экспортируются/импортируются по умолчанию, а в отношении динамических компонентов применяется именованный экспорт/импорт.

client/Form.js:

export default /*html*/ `<div id="form">  <input      type="text"      autocomplete="off"      autofocus      id="input"  >  <button    class="btn"    data-btn="add"  >    Add  </button></div>`

/*html*/ обеспечивает подсветку синтаксиса, предоставляемую расширением для VSCode es6-string-html. Атрибут data-btn позволит идентифицировать кнопку в скрипте.

Обратите внимание, что глобальные атрибуты id позволяют обращаться к DOM-элементам напрямую. Дело в том, что такие элементы (с идентификаторами), при разборе и отрисовке документа становятся глобальными переменными (свойствами глобального объекта window). Разумеется, значения идентификаторов должны быть уникальными для документа.

client/Buttons.js:

export default /*html*/ `<div id="buttons">  <button    class="btn filter checked"    data-btn="all"  >    All  </button>  <button    class="btn filter"    data-btn="active"  >    Active  </button>  <button    class="btn filter"    data-btn="completed"  >    Completed  </button></div>`

Кнопки для фильтрации тудушек позволят отображать все, активные (невыполненные) и завершенные (выполненные) задачи.

client/Item.js (самый сложный компонент с точки зрения структуры):

/** * функция принимает на вход задачу, * которая представляет собой объект, * включающий идентификатор, текст и индикатор выполнения * * индикатор выполнения управляет дополнительными классами * и текстом кнопки завершения задачи * * текст завершенной задачи должен быть перечеркнут, * а кнопка для изменения (обновления) текста такой задачи - отключена * * завершенную задачу можно сделать активной*/export const Item = ({ id, text, done }) => /*html*/ `<li  class="item ${done ? 'completed' : ''}"  data-id="${id}">  <button    class="btn ${done ? 'warning' : 'success'}"    data-btn="complete"  >    ${done ? 'Cancel' : 'Complete'}  </button>  <span class="text">    ${text}  </span>  <button    class="btn info ${done ? 'disabled' : ''}"    data-btn="update"  >    Update  </button>  <button    class="btn danger"    data-btn="delete"  >    Delete  </button></li>`

client/List.js:

/** * для формирования списка используется компонент Item * * функция принимает на вход список задач * * если вам не очень понятен принцип работы reduce * https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce*/import { Item } from "./Item.js"export const List = (todos) => /*html*/ `  <ul id="list">    ${todos.reduce(      (html, todo) =>        (html += `            ${Item(todo)}        `),      ''    )}  </ul>`

С компонентами закончили.

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

src/helpers.js:

/** * данная функция будет использоваться * для визуализации нажатия одной из кнопок * для фильтрации задач * * она принимает элемент - нажатую кнопку и класс - в нашем случае checked * * основной контейнер имеет идентификатор root, * поэтому мы можем обращаться к нему напрямую * из любой части кода, в том числе, из модулей*/export const toggleClass = (element, className) => {  root.querySelector(`.${className}`).classList.remove(className)  element.classList.add(className)}// примерные задачиexport const todosExample = [  {    id: '1',    text: 'Learn HTML',    done: true  },  {    id: '2',    text: 'Learn CSS',    done: true  },  {    id: '3',    text: 'Learn JavaScript',    done: false  },  {    id: '4',    text: 'Stay Alive',    done: false  }]

Создадим базу данных (пока в форме локального хранилища).

src/storage.js:

/** * база данных имеет два метода * get - для получения тудушек * set - для записи (сохранения) тудушек*/export default (() => ({  get: () => JSON.parse(localStorage.getItem('todos')),  set: (todos) => { localStorage.setItem('todos', JSON.stringify(todos)) }}))()

Побаловались и хватит. Приступаем к делу.

src/script.js:

// импортируем компоненты, вспомогательную функцию, примерные задачи и хранилищеimport Form from './components/Form.js'import Buttons from './components/Buttons.js'import { List } from './components/List.js'import { Item } from './components/Item.js'import { toggleClass, todosExample } from './src/helpers.js'import storage from './src/storage.js'// функция принимает контейнер и список задачconst App = (root, todos) => {  // формируем разметку с помощью компонентов и дополнительных элементов  root.innerHTML = `    <h1 id="title">      JS Todos App    </h1>    ${Form}    <h3 id="counter"></h3>    ${Buttons}    ${List(todos)}  `  // обновляем счетчик  updateCounter()  // получаем кнопку добавления задачи в список  const $addBtn = root.querySelector('[data-btn="add"]')  // основной функционал приложения  // функция добавления задачи в список  function addTodo() {    if (!input.value.trim()) return    const todo = {      // такой способ генерации идентификатора гарантирует его уникальность и соответствие спецификации      id: Date.now().toString(16).slice(-4).padStart(5, 'x'),      text: input.value,      done: false    }    list.insertAdjacentHTML('beforeend', Item(todo))    todos.push(todo)    // очищаем поле и устанавливаем фокус    clearInput()    updateCounter()  }  // функция завершения задачи  // принимает DOM-элемент списка  function completeTodo(item) {    const todo = findTodo(item)    todo.done = !todo.done    // рендерим только изменившийся элемент    renderItem(item, todo)    updateCounter()  }  // функция обновления задачи  function updateTodo(item) {    item.classList.add('disabled')    const todo = findTodo(item)    const oldValue = todo.text    input.value = oldValue    // тонкий момент: мы используем одну и ту же кнопку    // для добавления задачи в список и обновления текста задачи    $addBtn.textContent = 'Update'    // добавляем разовый обработчик    $addBtn.addEventListener(      'click',      (e) => {        // останавливаем распространение события для того,        // чтобы нажатие кнопки не вызвало функцию добавления задачи в список        e.stopPropagation()        const newValue = input.value.trim()        if (newValue && newValue !== oldValue) {          todo.text = newValue        }        renderItem(item, todo)        clearInput()        $addBtn.textContent = 'Add'      },      { once: true }    )  }  // функция удаления задачи  function deleteTodo(item) {    const todo = findTodo(item)    item.remove()    todos.splice(todos.indexOf(todo), 1)    updateCounter()  }  // функция поиска задачи  function findTodo(item) {    const { id } = item.dataset    const todo = todos.find((todo) => todo.id === id)    return todo  }  // дополнительный функционал  // функция фильтрации задач  // принимает значение кнопки  function filterTodos(value) {    const $items = [...root.querySelectorAll('.item')]    switch (value) {      // отобразить все задачи      case 'all':        $items.forEach((todo) => (todo.style.display = ''))        break      // активные задачи      case 'active':        // отобразить все и отключить завершенные        filterTodos('all')        $items          .filter((todo) => todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break      // завершенные задачи      case 'completed':        // отобразить все и отключить активные        filterTodos('all')        $items          .filter((todo) => !todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break    }  }  // функция обновления счетчика  function updateCounter() {    // считаем количество невыполненных задач    const count = todos.filter((todo) => !todo.done).length    counter.textContent = `      ${count > 0 ? `${count} todo(s) left` : 'All todos completed'}    `    if (!todos.length) {      counter.textContent = 'There are no todos'      buttons.style.display = 'none'    } else {      buttons.style.display = ''    }  }  // функция повторного рендеринга изменившегося элемента  function renderItem(item, todo) {    item.outerHTML = Item(todo)  }  // функция очистки инпута  function clearInput() {    input.value = ''    input.focus()  }  // делегируем обработку событий корневому узлу  root.onclick = ({ target }) => {    if (target.tagName !== 'BUTTON') return    const { btn } = target.dataset    if (target.classList.contains('filter')) {      filterTodos(btn)      toggleClass(target, 'checked')    }    const item = target.parentElement    switch (btn) {      case 'add':        addTodo()        break      case 'complete':        completeTodo(item)        break      case 'update':        updateTodo(item)        break      case 'delete':        deleteTodo(item)        break    }  }  // обрабатываем нажатие Enter  document.onkeypress = ({ key }) => {    if (key === 'Enter') addTodo()  }  // оптимизация работы с хранилищем  window.onbeforeunload = () => {    storage.set(todos)  }}// инициализируем приложения;(() => {  // получаем задачи из хранилища  let todos = storage.get('todos')  // если в хранилище пусто  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

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

Однако, с использованием локального хранилища в качестве базы данных сопряжено несколько проблем: 1) ограниченный размер около 5 Мб, зависит от браузера; 2) потенциальная возможность потери данных при очистке хранилищ браузера, например, при очистке истории просмотра страниц, нажатии кнопки Clear site data вкладки Application Chrome DevTools и т.д.; 3) привязка к браузеру невозможность использовать приложение на нескольких устройствах.

Первую проблему (ограниченность размера хранилища) можно решить с помощью IndexedDB.

Индексированная база данных имеет довольно сложный интерфейс, поэтому воспользуемся абстракцией Jake Archibald idb-keyval. Копируем этот код и записываем его в файл src/idb.js.

Вносим в src/script.js следующие изменения:

// import storage from './src/storage.js'import { get, set } from './src/idb.js'window.onbeforeunload = () => {  // storage.set(todos)  set('todos', todos)}// обратите внимание, что функция инициализации приложения стала асинхронной;(async () => {  // let todos = storage.get('todos')  let todos = await get('todos')  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

Вторую и третью проблемы можно решить только с помощью удаленной базы данных. В качестве таковой мы будем использовать облачную MongoDB. Преимущества ее использования заключаются в отсутствии необходимости предварительной установки и настройки, а также в возможности доступа к данным из любого места. Из недостатков можно отметить отсутствие гарантии конфиденциальности данных. Однако, при желании, данные можно шифровать на клиенте перед отправкой на сервер или на сервере перед отправкой в БД.

React, Vue

Ниже приводятся примеры реализации клиентской части тудушки на React и Vue.

React:


Vue:


База данных


Перед тем, как создавать сервер, имеет смысл настроить базу данных. Тем более, что в этом нет ничего сложного. Алгоритм действий следующий:

  1. Создаем аккаунт в MongoDB Atlas
  2. Во вкладке Projects нажимаем на кнопку New Project
  3. Вводим название проекта, например, todos-db, и нажимаем Next
  4. Нажимаем Create Project
  5. Нажимаем Build a Cluster
  6. Нажимаем Create a cluster (FREE)
  7. Выбираем провайдера и регион, например, Azure и Hong Kong, и нажимаем Create Cluster
  8. Ждем завершения создания кластера и нажимаем connect
  9. В разделе Add a connection IP address выбираем либо Add Your Current IP Address, если у вас статический IP, либо Allow Access from Anywhere, если у вас, как в моем случае, динамический IP (если сомневаетесь, выбирайте второй вариант)
  10. Вводим имя пользователя и пароль, нажимаем Create Database User, затем нажимаем Choose a connection method
  11. Выбираем Connect your application
  12. Копируем строку из раздела Add your connection string into your application code
  13. Нажимаем Close












В корневой директории создаем файл .env и вставляем в него скопированную строку (меняем <username>, <password> и <dbname> на свои данные):

MONGO_URI=mongodb+srv://<username>:<password>@cluster0.hfvcf.mongodb.net/<dbname>?retryWrites=true&w=majority

Сервер


Находясь в корневой директории, инициализируем проект:

npm init -y// илиyarn init -yp

Устанавливаем основные зависимости:

yarn add cors dotenv express express-validator mongoose

  • cors отключает политику общего происхождения (одного источника)
  • dotenv предоставляет доступ к переменным среды в файле .env
  • express облегчает создание сервера на Node.js
  • express-validator служит для проверки (валидации) данных
  • mongoose облегчает работу с MongoDB

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

yarn add -D nodemon open-cli morgan

  • nodemon запускает сервер и автоматически перезагружает его при внесении изменений в файл
  • open-cli открывает вкладку браузера по адресу, на котором запущен сервер
  • morgan логгер HTTP-запросов

Далее добавляем в package.json скрипты для запуска сервера (dev для запуска сервера для разработки и start для продакшн-сервера):

"scripts": {  "start": "node index.js",  "dev": "open-cli http://localhost:1234 && nodemon index.js"},

Отлично. Создаем файл index.js следующего содержания:

// подключаем библиотекиconst express = require('express')const mongoose = require('mongoose')const cors = require('cors')const morgan = require('morgan')require('dotenv/config')// инициализируем приложение и получаем роутерconst app = express()const router = require('./server/router')// подключаем промежуточное ПОapp.use(express.json())app.use(express.urlencoded({ extended: false }))app.use(cors())app.use(morgan('dev'))// указываем, где хранятся статические файлыapp.use(express.static(__dirname))// подлючаемся к БДmongoose.connect(  process.env.MONGO_URI,  {    useNewUrlParser: true,    useUnifiedTopology: true,    useFindAndModify: false,    useCreateIndex: true  },  () => console.log('Connected to database'))// возвращаем index.html в ответ на запрос к корневому узлуapp.get('/', (_, res) => {  res.sendFile(__dirname + '/index.html')})// при запросе к api передаем управление роутеруapp.use('/api', router)// определяем порт и запускаем серверconst PORT = process.env.PORT || 1234app.listen(PORT, () => console.log(`Server is running`))

Тестируем сервер:

yarn dev// илиnpm run dev



Прекрасно, сервер работает. Теперь займемся маршрутизацией. Но перед этим определим схему данных, которые мы будем получать от клиента. Создаем директорию server для хранения серверных файлов. В этой директории создаем файлы Todo.js и router.js.

Структура проекта на данном этапе:

client  components    Buttons.js    Form.js    Item.js    List.js  src    helpers.js    idb.js    storage.js  script.js  style.cssserver  Todo.js  router.js.envindex.htmlindex.jspackage.jsonyarn.lock (либо package-lock.json)

Определяем схему в src/Todo.js:

const { Schema, model } = require('mongoose')const todoSchema = new Schema({  id: {    type: String,    required: true,    unique: true  },  text: {    type: String,    required: true  },  done: {    type: Boolean,    required: true  }})// экспорт модели данныхmodule.exports = model('Todo', todoSchema)

Настраиваем маршрутизацию в src/router.js:

// инициализируем роутерconst router = require('express').Router()// модель данныхconst Todo = require('./Todo')// средства валидацииconst { body, validationResult } = require('express-validator')/** * наш интерфейс (http://personeltest.ru/away/localhost:1234/api) * будет принимать и обрабатывать 4 запроса * GET-запрос /get - получение всех задач из БД * POST /add - добавление в БД новой задачи * DELETE /delete/:id - удаление задачи с указанным идентификатором * PUT /update - обновление текста или индикатора выполнения задачи * * для работы с БД используется модель Todo и методы * find() - для получения всех задач * save() - для добавления задачи * deleteOne() - для удаления задачи * updateOne() - для обновления задачи * * ответ на запрос - объект, в свойстве message которого * содержится сообщение либо об успехе операции, либо об ошибке*/// получение всех задачrouter.get('/get', async (_, res) => {  const todos = (await Todo.find()) || []  return res.json(todos)})// добавление задачиrouter.post(  '/add',  // пример валидации  [    body('id').exists(),    body('text').notEmpty().trim().escape(),    body('done').toBoolean()  ],  async (req, res) => {    // ошибки - это результат валидации    const errors = validationResult(req)    if (!errors.isEmpty()) {      return res.status(400).json({ message: errors.array()[0].msg })    }    const { id, text, done } = req.body    const todo = new Todo({      id,      text,      done    })    try {      await todo.save()      return res.status(201).json({ message: 'Todo created' })    } catch (error) {      return res.status(500).json({ message: `Error: ${error}` })    }  })// удаление задачиrouter.delete('/delete/:id', async (req, res) => {  try {    await Todo.deleteOne({      id: req.params.id    })    res.status(201).json({ message: 'Todo deleted' })  } catch (error) {    return res.status(500).json({ message: `Error: ${error}` })  }})// обновление задачиrouter.put(  '/update',  [    body('text').notEmpty().trim().escape(),    body('done').toBoolean()  ],  async (req, res) => {    const errors = validationResult(req)    if (!errors.isEmpty()) {      return res.status(400).json({ message: errors.array()[0].msg })    }    const { id, text, done } = req.body    try {      await Todo.updateOne(        {          id        },        {          text,          done        }      )      return res.status(201).json({ message: 'Todo updated' })    } catch (error) {      return res.status(500).json({ message: `Error: ${error}` })    }})// экспорт роутераmodule.exports = router

Интеграция


Возвращаемся к клиентской части. Для того, чтобы абстрагировать отправляемые клиентом запросы мы также прибегнем к помощи роутера. Создаем файл client/src/router.js:

/** * наш роутер - это обычная функция, * принимающая адрес конечной точки в качестве параметра (url) * * функция возвращает объект с методами: * get() - для получения всех задач из БД * set() - для добавления в БД новой задачи * update() - для обновления текста или индикатора выполнения задачи * delete() - для удаления задачи с указанным идентификатором * * все методы, кроме get(), принимают на вход задачу * * методы возвращают ответ от сервера в формате json * (объект со свойством message)*/export const Router = (url) => ({  // получение всех задач  get: async () => {    const response = await fetch(`${url}/get`)    return response.json()  },  // добавление задачи  set: async (todo) => {    const response = await fetch(`${url}/add`, {      method: 'POST',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(todo)    })    return response.json()  },  // обновление задачи  update: async (todo) => {    const response = await fetch(`${url}/update`, {      method: 'PUT',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(todo)    })    return response.json()  },  // удаление задачи  delete: async ({ id }) => {    const response = await fetch(`${url}/delete/${id}`, {      method: 'DELETE'    })    return response.json()  }})

Для того, чтобы сообщать пользователю о результате выполнения CRUD-операции (create, read, update, delete создание, чтение, обновление, удаление), добавим в src/helpers.js еще одну вспомогательную функцию:

// функция создает модальное окно с сообщением о результате операции// и удаляет его через две секундыexport const createModal = ({ message }) => {  root.innerHTML += `<div data-id="modal">${message}</div>`  const timer = setTimeout(() => {    root.querySelector('[data-id="modal"]').remove()    clearTimeout(timer)  }, 2000)}

Вот как выглядит итоговый вариант client/script.js:

import Form from './components/Form.js'import Buttons from './components/Buttons.js'import { List } from './components/List.js'import { Item } from './components/Item.js'import { toggleClass, createModal, todosExample } from './src/helpers.js'// импортируем роутер и передаем ему адрес конечной точкиimport { Router } from './src/router.js'const router = Router('http://localhost:1234/api')const App = (root, todos) => {  root.innerHTML = `    <h1 id="title">      JS Todos App    </h1>    ${Form}    <h3 id="counter"></h3>    ${Buttons}    ${List(todos)}  `  updateCounter()  const $addBtn = root.querySelector('[data-btn="add"]')  // основной функционал  async function addTodo() {    if (!input.value.trim()) return    const todo = {      id: Date.now().toString(16).slice(-4).padStart(5, 'x'),      text: input.value,      done: false    }    list.insertAdjacentHTML('beforeend', Item(todo))    todos.push(todo)    // добавляем в БД новую задачу и сообщаем о результате операции пользователю    createModal(await router.set(todo))    clearInput()    updateCounter()  }  async function completeTodo(item) {    const todo = findTodo(item)    todo.done = !todo.done    renderItem(item, todo)    // обновляем индикатор выполнения задачи    createModal(await router.update(todo))    updateCounter()  }  function updateTodo(item) {    item.classList.add('disabled')    const todo = findTodo(item)    const oldValue = todo.text    input.value = oldValue    $addBtn.textContent = 'Update'    $addBtn.addEventListener(      'click',      async (e) => {        e.stopPropagation()        const newValue = input.value.trim()        if (newValue && newValue !== oldValue) {          todo.text = newValue        }        renderItem(item, todo)        // обновляем текст задачи        createModal(await router.update(todo))        clearInput()        $addBtn.textContent = 'Add'      },      { once: true }    )  }  async function deleteTodo(item) {    const todo = findTodo(item)    item.remove()    todos.splice(todos.indexOf(todo), 1)    // удаляем задачу    createModal(await router.delete(todo))    updateCounter()  }  function findTodo(item) {    const { id } = item.dataset    const todo = todos.find((todo) => todo.id === id)    return todo  }  // дальше все тоже самое  // за исключением window.onbeforeunload  function filterTodos(value) {    const $items = [...root.querySelectorAll('.item')]    switch (value) {      case 'all':        $items.forEach((todo) => (todo.style.display = ''))        break      case 'active':        filterTodos('all')        $items          .filter((todo) => todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break      case 'completed':        filterTodos('all')        $items          .filter((todo) => !todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break    }  }  function updateCounter() {    const count = todos.filter((todo) => !todo.done).length    counter.textContent = `      ${count > 0 ? `${count} todo(s) left` : 'All todos completed'}    `    if (!todos.length) {      counter.textContent = 'There are no todos'      buttons.style.display = 'none'    } else {      buttons.style.display = ''    }  }  function renderItem(item, todo) {    item.outerHTML = Item(todo)  }  function clearInput() {    input.value = ''    input.focus()  }  root.onclick = ({ target }) => {    if (target.tagName !== 'BUTTON') return    const { btn } = target.dataset    if (target.classList.contains('filter')) {      filterTodos(btn)      toggleClass(target, 'checked')    }    const item = target.parentElement    switch (btn) {      case 'add':        addTodo()        break      case 'complete':        completeTodo(item)        break      case 'update':        updateTodo(item)        break      case 'delete':        deleteTodo(item)        break    }  }  document.onkeypress = ({ key }) => {    if (key === 'Enter') addTodo()  }};(async () => {  // получаем задачи из БД  let todos = await router.get()  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

Поздравляю, вы только что создали полноценную фуллстек-тудушку.

TypeScript

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



Заключение


Подведем краткие итоги.

Мы с вами реализовали полноценное клиент-серверное приложение для добавления, редактирования и удаления задач из списка, интегрированное с настоящей базой данных. На клиенте мы использовали самый современный (чистый) JavaScript, на сервере Node.js сквозь призму Express.js, для взаимодействия с БД Mongoose. Мы рассмотрели парочку вариантов хранения данных на стороне клиента (local storage, indexeddb idb-keyval). Также мы увидели примеры реализации клиентской части на React (+TypeScript) и Vue. По-моему, очень неплохо для одной статьи.

Буду рад любой форме обратной связи. Благодарю за внимание и хорошего дня.
Источник: habr.com
К списку статей
Опубликовано: 24.12.2020 12:19:48
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Javascript

Reactjs

Vuejs

Typescript

React

Vue

Express

Mongoose

Mongodb

Fullstack

Todos

Todo app

Todo

Категории

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

  • Имя: Макс
    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