Создание современного проекта требует разделения логики на внешний и внутренний код. Причиной этого шага является повышение возможности повторного использования кода. Например, нам может понадобиться создать собственное мобильное приложение, которое обращается к внутреннему API. Или мы можем разрабатывать модуль, который станет частью большой модульной платформы.
Оператор, сидящий за старомодным телефонным коммутатором — создайте
Популярным способом создания серверного API является использование Node.js с такой библиотекой, как Express или Restify. Эти библиотеки упрощают создание маршрутов RESTful. Проблема с этими библиотеками заключается в том, что нам придется писать тонны повторяющегося кода. Нам также потребуется написать код для авторизации и другой логики промежуточного программного обеспечения.
Чтобы избежать этой дилеммы, мы можем использовать такую структуру, как Feathers, которая поможет нам сгенерировать API всего за несколько команд.
Что делает Feathers удивительным, так это его простота. Вся структура является модульной, и нам нужно установить только те функции, которые нам нужны. Сам Feathers — это тонкая оболочка, построенная поверх Express, куда были добавлены новые функции — сервисы и хуки. Feathers также позволяет нам легко отправлять и получать данные через WebSockets.
Предпосылки
Чтобы следовать этому руководству, на вашем компьютере должны быть установлены следующие компоненты:
Node.js v12+ и актуальная версия npm. Ознакомьтесь с этим руководством, если вам нужна помощь в настройке.
МонгоБД v4.2+. Ознакомьтесь с этим руководством, если вам нужна помощь в настройке.
Менеджер пакетов Yarn — устанавливается с помощью npm i -g yarn.
Также будет полезно, если вы знакомы со следующими темами:
Как писать современный JavaScript
Управление потоком в современном JavaScript (например async... await)
Основы Реакта
Основы REST API
Также обратите внимание, что готовый код проекта вы можете найти на GitHub.
Подготовить приложение
Мы собираемся создать приложение для управления контактами CRUD, используя Node.js, React, Feathers и MongoDB.
В этом уроке я покажу вам, как создать приложение снизу вверх. Мы начнем наш проект с помощью популярного инструмента Create React App.
Вы можете установить его так:
npm install -g
Затем создайте новый проект:
# scaffold a new react project
cd
# delete unnecessary files
rm src/logo.svg src/App.css src/serviceWorker.js
Используйте свой любимый редактор кода и удалите все содержимое в файлах src/index.css. Затем откройте src/App.jsи перепишите код следующим образом:
import React from ‘react’;
const App = () => {
return (
Contact Manager
) ;
};
export default App;
И в src/index.js, измените код следующим образом:
import React from ‘react’;
import ReactDOM from ‘
import '. /index.css’;
import App from '. /App’;
ReactDOM.render (
,
document.getElementById (‘root’)
) ;
Запустите yarn startиз
Создайте сервер API с Feathers
Давайте приступим к созданию внутреннего API для нашего проекта CRUD с помощью
# Install Feathers
npm install @feathersjs/cli -g
# Create directory for the
# Run this command in the `
mkdir backend
cd backend
# Generate a feathers
feathers generate app
? Do you want to use JavaScript or TypeScript? JavaScript
? Project name backend
? Description Contacts API server
? What folder should the source files live in? src
? Which package manager are you using (has to be installed globally)? Yarn
? What type of API are you making? REST, Realtime via Socket.io
? Which testing framework do you prefer? Mocha + assert
? This app uses authentication No
? Which coding style do you want to use? ESLint
# Ensure Mongodb is running
sudo service mongod start
sudo service mongod status
● mongod.service — MongoDB Database Server
Loaded: loaded (/lib/systemd/system/mongod.service; disabled; vendor preset: enabled)
Active: active (running) since Fri
Docs: https://docs.mongodb.org/manual
Main PID: 31043 (mongod)
CGroup: /system.slice/mongod.service
└─31043 /usr/bin/mongod —config /etc/mongod.conf
# Generate RESTful routes for Contact Model
feathers generate service
? What kind of service is it? Mongoose
? What is the name of the service? contacts
? Which path should the service be registered on? /contacts
? What is the database connection string? mongodb: //localhost:27017/contactsdb
# Install email and unique field validation
yarn add
Откроем backend/config/default.json. Здесь мы можем настроить параметры подключения к MongoDB и другие параметры. Измените значение разбивки на страницы по умолчанию на 50, так как интерфейсная разбивка на страницы не будет рассматриваться в этом руководстве:
{
«host»: «localhost»,
«port»: 3030,
«public»: «.../public/»,
«paginate»: {
«default»: 50,
«max»: 50
},
«mongodb»: «mongodb: //localhost:27017/contactsdb»
}
Откройте backend/src/models/contact.model.jsи обновите код следующим образом:
require (‘
module.exports = function (app) {
const modelName = ‘contacts’;
const mongooseClient = app.get (‘mongooseClient’) ;
const { Schema } = mongooseClient;
const schema = new Schema ({
name: {
first: {
type: String,
required: [true, ‘First Name is required’]
},
last: {
type: String,
required: false
}
},
email: {
type: mongooseClient.SchemaTypes.Email,
required: [true, ‘Email is required’]
},
phone: {
type: String,
required: [true, ‘Phone is required’],
validate: {
validator: function (v) {
return /^\+ (?:[0−9]?){6,14}[0−9]$/.test (v) ;
},
message: '{VALUE} is not a valid international phone number!'
}
}
}, {
timestamps: true
}) ;
// This is necessary to avoid model compilation errors in watch mode
// see https://mongoosejs.com/docs/api/connection.html#connection_Connection-deleteModel
if (mongooseClient.modelNames ().includes (modelName)) {
mongooseClient.deleteModel (modelName) ;
}
return mongooseClient.model (modelName, schema) ;
};
Mongoose представляет новую функцию под названием timestamps, которая вставляет для вас два новых поля — createdAtи updatedAt. Эти два поля будут заполняться автоматически всякий раз, когда мы создаем или обновляем запись. Мы также установили плагин
Теперь откройте backend/src/mongoose.jsи измените эту строку:
{ useCreateIndex: true, useNewUrlParser: true }
к:
{
useCreateIndex: true,
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
}
Это избавит вас от нескольких надоедливых предупреждений об устаревании.
Откройте новый терминал и выполните yarn testвнутри backendкаталога. Все тесты должны пройти успешно. Затем продолжайте и выполните yarn start, чтобы запустить внутренний сервер. После инициализации сервера он должен вывести ‘Feathers application started on localhost:3030‘на консоль.
Запустите браузер и перейдите по
{«total»:0,«limit»:50,«skip»:0,«data»:[]}
Протестируйте API с помощью Hoppscotch
Теперь давайте используем Hoppscotch (ранее Postwoman), чтобы убедиться, что все наши конечные точки работают правильно.
{
«_id»: «5f64832c20745f4f282b39f9»,
«name»: {
«first»: «Tony»,
«last»: «Stark»
},
«phone»: «+18138683770»,
«email»: «tony@starkenterprises.com»,
«createdAt»: «
«updatedAt»: «
«__v»: 0
}
Теперь давайте извлечем наш вновь созданный контакт. Эта ссылка откроет Hoppscotch, готовый отправить запрос GET на /contactsконечную точку. Когда вы нажмете кнопку «Отправить «, вы должны получить такой ответ:
{
«total»: 1,
«limit»: 50,
«skip»: 0,
«data»: [
{
«_id»: «5f64832c20745f4f282b39f9»,
«name»: {
«first»: «Tony»,
«last»: «Stark»
},
«phone»: «+18138683770»,
«email»: «tony@starkenterprises.com»,
«createdAt»: «
«updatedAt»: «
«__v»: 0
}
]
}
Мы можем показать отдельный контакт в Hoppscotch, отправив запрос GET на адрес http: //localhost:3030/contacts/<_id>. Поле _idвсегда будет уникальным, поэтому вам нужно скопировать его из ответа, полученного на предыдущем шаге. Это ссылка на приведенный выше пример. Нажатие «Отправить «покажет контакт.
Мы можем обновить контакт, отправив запрос PUT http: //localhost:3030/contacts/<_id>и передав ему обновленные данные в виде JSON. Это ссылка на приведенный выше пример. Нажатие «Отправить «обновит контакт.
Наконец, мы можем удалить наш контакт, отправив DELETEзапрос на тот же адрес, то есть http: //localhost:3030/contacts/<_id>. Это ссылка на приведенный выше пример. Нажатие «Отправить «удалит контакт.
Hoppscotch — очень универсальный инструмент, и я рекомендую вам использовать его, чтобы убедиться, что ваш API работает должным образом, прежде чем переходить к следующему шагу.
Создайте пользовательский интерфейс
Изначально я хотел использовать Semantic UI для оформления, но на момент написания статьи он не обновлялся более двух лет. К счастью, сообществу
Мы также будем использовать Semantic UI React для быстрого создания нашего пользовательского интерфейса без необходимости определять множество имен классов. К счастью, этот проект также поддерживается в актуальном состоянии.
Наконец, мы будем использовать React Router для обработки маршрутизации.
После этого откройте новый терминал в
# Install Fomantic UI CSS and Semantic UI React
yarn add
# Install React Router
yarn add
Обновите структуру проекта, добавив в каталог следующие каталоги и файлы src:
src
├── App.js
├── App.test.js
├── components # (new)
│ ├── contact-form.js # (new)
│ └── contact-list.js # (new)
├── index.css
├── index.js
├── pages # (new)
│ ├── contact-form-page.js # (new)
│ └── contact-list-page.js # (new)
├── serviceWorker.js
└── setupTests.js
Из терминала:
cd src
mkdir pages components
touch components/
touch pages/
Давайте быстро заполним файлы JavaScript некоторым
Компонент ContactListбудет функциональным компонентом (обычная функция JavaScript, которая возвращает элемент React):
// src/components/
import React from ‘react’;
const ContactList = () => {
return (
No contacts here
) ;
}
export default ContactList;
Для контейнеров верхнего уровня я использую страницы. Давайте предоставим некоторый код для ContactListPageкомпонента:
// src/pages/
import React from ‘react’;
import ContactList from '.../components/
const ContactListPage = () => {
return (
List of Contacts
) ;
};
export default ContactListPage;
Компонент ContactFormдолжен быть умным, так как он должен управлять своим состоянием, в частности формировать поля. Мы будем делать это с помощью хуков React:
// src/components/
import React from ‘react’;
const ContactForm = () => {
return (
Form under construction
)
}
export default ContactForm;
Заполните ContactFormPageкомпонент этим кодом:
// src/pages/
import React from ‘react’;
import ContactForm from '.../components/
const ContactFormPage = () => {
return (
) ;
};
export default ContactFormPage;
Теперь давайте создадим меню навигации и определим маршруты для нашего приложения. App.jsчасто упоминается как «шаблон макета» для одностраничного приложения:
// src/App.js
import React from ‘react’;
import { NavLink, Route } from ‘
import { Container } from ‘
import ContactListPage from '. /pages/
import ContactFormPage from '. /pages/
const App = () => {
return (
Contacts List
<NavLink
className="item"
activeClassName="active"
exact
to="/contacts/new"
>
Add Contact
) ;
};
export default App;
В приведенном выше коде используется React Router. Если вы хотите освежить в памяти этот вопрос, обратитесь к нашему руководству.
Наконец, обновите src/index.jsфайл с этим кодом, где мы импортируем
// src/index.js
import React from ‘react’;
import ReactDOM from ‘
import { BrowserRouter } from ‘
import App from '. /App’;
import ‘
import '. /index.css’;
ReactDOM.render (
,
document.getElementById (‘root’)
) ;
Убедитесь, что
Скриншот пустого списка контактов
Управление состоянием с помощью React Hooks и Context API
Раньше можно было использовать Redux, когда ему приходилось управлять состоянием в приложении React. Однако, начиная с React v16.8.0, можно управлять глобальным состоянием в приложении React с помощью React Hooks и Context API.
Используя эту новую технику, вы будете писать меньше кода, который легче поддерживать. Мы
Далее давайте рассмотрим подключение Context API.
Определить хранилище контекста
Это будет похоже на наш магазин для обработки глобального состояния контактов. Наше состояние будет состоять из нескольких переменных, включая contactsмассив, loadingсостояние и messageобъект для хранения сообщений об ошибках, сгенерированных внутренним
В srcкаталоге создайте contextпапку, содержащую
cd src
mkdir context
touch context/
И вставьте следующий код:
import React, { useReducer, createContext } from ‘react’;
export const ContactContext = createContext () ;
const initialState = {
contacts: [],
contact: {}, // selected or new
message: {}, // { type: ‘success|fail’, title:‘Info|Error’ content:‘lorem ipsum’}
};
function reducer (state, action) {
switch (action.type) {
case ‘FETCH_CONTACTS’: {
return {
...state,
contacts: action.payload,
};
}
default:
throw new Error () ;
}
}
export const ContactContextProvider = props => {
const [state, dispatch] = useReducer (reducer, initialState) ;
const { children } = props;
return (
{children}
) ;
};
Как видите, мы используем хук useReducer, который является альтернативой useState. useReducerподходит для обработки сложной логики состояния, включающей несколько подзначений. Мы также используем Context API, чтобы разрешить обмен данными с другими компонентами React.
Внедрить поставщика контекста в корень приложения
Нам нужно инкапсулировать наш корневой компонент с расширением Context Provider. Обновить src/index.jsследующим образом:
...
import { ContactContextProvider } from '. /context/
ReactDOM.render (
,
document.getElementById (‘root’)
) ;
Теперь все дочерние компоненты смогут получить доступ к глобальному состоянию с помощью useContextхука.
Показать список контактов
На этом этапе мы создадим некоторые статические данные для тестирования. Наше начальное состояние имеет пустой массив контактов. Мы будем использовать этот dispatchметод для временного заполнения contactsмассива. Откройте pages/
import React, { useContext, useEffect } from ‘react’;
import ContactList from '.../components/
import { ContactContext } from '.../context/
const data = [
{
_id: '1',
name: {
first: ‘John’,
last: ‘Doe’,
},
phone: '555',
email: ‘john@gmail.com’,
},
{
_id: '2',
name: {
first: ‘Bruce’,
last: ‘Wayne’,
},
phone: '777',
email: ‘bruce.wayne@gmail.com’,
},
];
const ContactListPage = () => {
const [state, dispatch] = useContext (ContactContext) ;
useEffect (() => {
dispatch ({
type: ‘FETCH_CONTACTS’,
payload: data,
}) ;
}, [dispatch]) ;
return (
List of Contacts
) ;
};
export default ContactListPage;
Далее мы будем использовать простой цикл для отображения контактов в формате components/
import React from ‘react’;
const ContactList = ({ contacts }) => {
const list = () => {
return contacts.map (contact => {
return (
-
{contact.name.first} {contact.name.last}
) ;
}) ;
};
return (
- {list () }
) ;
}
export default ContactList;
Теперь, если вы вернетесь в браузер, у вас должно получиться
Скриншот списка контактов с двумя контактами
Давайте сделаем пользовательский интерфейс списка более привлекательным, используя семантический стиль пользовательского интерфейса. В src/componentsпапке создайте новый файл
touch src/components/
Затем добавьте следующий код:
// src/components/
import React from ‘react’;
import { Card, Button, Icon } from ‘
const ContactCard = ({ contact }) => {
return (
) ;
}
export default ContactCard;
Обновите ContactListкомпонент, чтобы использовать новый ContactCardкомпонент:
// src/components/
import React from ‘react’;
import { Card } from ‘
import ContactCard from '. /
const ContactList = ({ contacts }) => {
const cards = () => {
return contacts.map (contact => {
return
}) ;
};
return
}
export default ContactList;
Теперь страница со списком должна выглядеть так:
Два контакта, отображаемые с помощью стилей семантического пользовательского интерфейса
Асинхронно получать данные с сервера API Feathers
Теперь, когда мы знаем, что глобальное состояние правильно передается другим компонентам React, мы можем сделать реальный запрос на выборку в базу данных и использовать данные для заполнения нашей страницы со списком контактов. Есть несколько способов сделать это, но способ, который я вам покажу, удивительно прост.
Затем установите библиотеку axios. Мы будем использовать это, чтобы сделать наши запросы:
yarn add axios
Затем обновите, src/
// src/
import React, { useContext, useEffect } from ‘react’;
import axios from ‘axios’;
import ContactList from '.../components/
import { ContactContext } from '.../context/
const ContactListPage = () => {
const [state, dispatch] = useContext (ContactContext) ;
useEffect (() => {
const fetchData = async () => {
const response = await axios.get (‘http: //localhost:3030/contacts’) ;
dispatch ({
type: ‘FETCH_CONTACTS’,
payload: response.data.data || response.data, // in case pagination is disabled
}) ;
};
fetchData () ;
}, [dispatch]) ;
return (
List of Contacts
) ;
}
export default ContactListPage;
После сохранения вернитесь в браузер. На странице списка контактов теперь должны отображаться данные из базы данных.
Обработка ошибок
Предположим, вы забыли запустить внутренний сервер и службу базы данных Mongo. Если запустить
Давайте реализуем некоторую обработку ошибок, сначала создав компонент, который будет отображать сообщения об ошибках. Мы также реализуем вспомогательную функцию для извлечения информации из перехваченных ошибок. Эта вспомогательная функция будет способна различать сетевые ошибки и сообщения об ошибках, отправленные внутренним сервером, например сообщения об ошибке проверки или ошибки 404.
Мы будем использовать компонент Semantic UI React Message для создания нашего кода. Создайте
touch src/components/
Затем вставьте следующий код:
// src/components/
import React from ‘react’;
import { Message } from ‘
export const FlashMessage = ({ message }) => {
return (
<Message
positive={message.type === ‘success’}
negative={message.type === ‘fail’}
header={message.title}
content={message.content}
/>
) ;
}
export const flashErrorMessage = (dispatch, error) => {
const err = error.response? error.response.data: error; // check if server or network error
dispatch ({
type: ‘FLASH_MESSAGE’,
payload: {
type: ‘fail’,
title: err.name,
content: err.message,
},
}) ;
}
Затем добавьте этот редюсер src/context/
function reducer (state, action) {
switch (action.type) {
...
case ‘FLASH_MESSAGE’: {
return {
...state,
message: action.payload,
};
}
...
}
}
Наконец, обновите pages/
// src/pages/
...
import { FlashMessage, flashErrorMessage } from '.../components/
const ContactListPage = () => {
const [state, dispatch] = useContext (ContactContext) ;
useEffect (() => {
const fetchData = async () => {
try {
const response = await axios.get (‘http: //localhost:3030/contacts’) ;
dispatch ({
type: ‘FETCH_CONTACTS’,
payload: response.data.data || response.data, // in case pagination is disabled
}) ;
} catch (error) {
flashErrorMessage (dispatch, error) ;
}
};
fetchData () ;
}, [dispatch]) ;
return (
List of Contacts
{state.message.content &&
) ;
}
export default ContactListPage;
Ниже приведен снимок экрана с сообщением об ошибке, которое возникает, когда внутренний сервер работает, но служба базы данных Mongo остановлена:
пример сообщения об ошибке
Обратите внимание, что для устранения вышеуказанной ошибки необходимо сначала запустить службу Mongo, а затем внутренний сервер Feathers в указанном порядке.
Обработка запросов на создание с использованием форм React Hook
Далее давайте посмотрим, как добавлять новые контакты, а для этого нам нужны формы. На первый взгляд построение формы кажется довольно простым. Но когда мы начинаем думать о проверке на стороне клиента и контроле того, когда должны отображаться ошибки, это становится сложным. Кроме того, внутренний сервер выполняет собственную проверку, и нам также потребуется отобразить эти ошибки в форме.
Вместо того, чтобы реализовывать всю функциональность форм самостоятельно, мы воспользуемся помощью библиотеки форм — React Hook Form — которая, на мой взгляд, является самой простой библиотекой для работы при создании форм React. Мы также будем использовать пакет classnames для выделения полей формы с ошибками проверки.
Прежде всего, остановите
yarn add
Перезапустите сервер после завершения установки пакетов.
Добавьте этот класс CSS в src/index.cssфайл, чтобы стилизовать ошибки формы:
.error {
color: #9f3a38;
}
Затем откройте src/components/
// src/components/
import React, { useContext } from ‘react’;
import { Form, Grid, Button } from ‘
import { useForm } from ‘
import classnames from ‘classnames’;
import { ContactContext } from '.../context/
const ContactForm = () => {
const [state] = useContext (ContactContext) ;
const { register, errors, handleSubmit } = useForm () ;
const onSubmit = data => console.log (data) ;
return (
Add New Contact
First Name
<input
id="name.first"
name="name.first"
type="text"
placeholder="First Name"
ref={register ({ required: true, minLength: 2 }) }
/>
{errors.name &&
errors.name.first.type === ‘required’ &&
‘You need to provide First Name’}
{errors.name &&
errors.name.first.type === ‘minLength’ &&
‘Must be 2 or more characters’}
Last Name
<input
id="name.last"
name="name.last"
type="text"
placeholder="Last Name"
ref={register}
/>
Phone
<input
id="phone"
name="phone"
type="text"
placeholder="Phone"
ref={register ({
required: true,
pattern: /^\+ (?:[0−9]?){6,14}[0−9]$/,
}) }
/>
{errors.phone &&
errors.phone.type === ‘required’ &&
‘You need to provide a Phone number’}
{errors.phone &&
errors.phone.type === ‘pattern’ &&
‘Phone number must be in International format’}
<input
id="email"
name="email"
type="text"
placeholder="Email"
ref={register ({
required: true,
pattern: /^\w+ ([. -]? \w+) *@\w+ ([. -]? \w+) * (\. \w{2,3}) +$/,
}) }
/>
{errors.email &&
errors.email.type === ‘required’ &&
‘You need to provide an Email address’}
{errors.email &&
errors.email.type === ‘pattern’ &&
‘Invalid email address’}
) ;
}
export default ContactForm;
Потратьте время на изучение кода; там много всего происходит. См. руководство по началу работы, чтобы понять, как работает React Hook Form. Кроме того, взгляните на документацию формы Semantic UI React и посмотрите, как мы использовали ее для создания нашей формы. Обратите внимание, что в нашем onSubmitобработчике мы выводим данные формы на консоль.
Теперь вернемся в браузер и попробуем намеренно сохранить неполную форму. Используя меню навигации, которое мы настроили ранее, нажмите кнопку «Добавить контакт «, затем нажмите кнопку «Сохранить «, не заполняя форму. Это должно вызвать следующие сообщения об ошибках проверки:
Ошибки проверки на стороне клиента
Теперь вы можете приступить к заполнению формы. По мере ввода вы заметите, что различные сообщения проверки меняются или исчезают. Как только все будет верно, вы можете снова нажать «Сохранить «. Если вы проверите вывод своей консоли, вы должны получить объект JSON, подобный этой структуре:
{
«name»: {
«first»: «Jason»,
«last»: «Bourne»
},
«phone»: «+1 555 555»,
«email»: «jason@gmail.com»
}
Давайте теперь определим необходимые действия для сохранения нового контакта в базе данных.
function reducer (state, action) {
switch (action.type) {
...
case ‘CREATE_CONTACT’: {
return {
...state,
contacts: [...state.contacts, action.payload],
message: {
type: ‘success’,
title: ‘Success’,
content: ‘New Contact created!',
},
};
}
...
}
}
Затем откройте src/components/
import React, { useContext, useState } from ‘react’;
import { Form, Grid, Button } from ‘
import { useForm } from ‘
import classnames from ‘classnames’;
import axios from ‘axios’;
import { Redirect } from ‘
import { ContactContext } from '.../context/
import { flashErrorMessage } from '. /
const ContactForm = () => {
const [state, dispatch] = useContext (ContactContext) ;
const { register, errors, handleSubmit } = useForm () ;
const [redirect, setRedirect] = useState (false) ;
const createContact = async data => {
try {
const response = await axios.post (‘http: //localhost:3030/contacts’, data) ;
dispatch ({
type: ‘CREATE_CONTACT’,
payload: response.data,
}) ;
setRedirect (true) ;
} catch (error) {
flashErrorMessage (dispatch, error) ;
}
};
const onSubmit = async data => {
await createContact (data) ;
};
if (redirect) {
return
}
return (
//... form code
)
}
export default ContactForm;
Мы создали отдельную createContactфункцию для управления созданием новых контактов. Позже мы реализуем еще одну функцию для обновления существующих контактов. В случае возникновения ошибки, будь то ошибка сети или сервера, отобразится быстрое сообщение, указывающее пользователю, что пошло не так. В противном случае, если запрос POST будет успешным, /будет выполнено перенаправление на. После этого на главной странице появится сообщение об успешном завершении.
Теперь завершите заполнение формы. После нажатия «Сохранить «мы должны быть перенаправлены на страницу со списком. В приведенном ниже примере я успешно добавил еще два контакта.
список контактов с тремя карточками контактов
Редактировать существующие контакты
Теперь, когда мы можем добавлять новые контакты, давайте посмотрим, как мы можем обновить существующие контакты. Давайте начнем с определения пары редюсеров для извлечения одного контакта и обновления контакта.
Обновить src/context/
function reducer (state, action) {
switch (action.type) {
...
case ‘FETCH_CONTACT’: {
return {
...state,
contact: action.payload,
};
}
case ‘UPDATE_CONTACT’: {
const contact = action.payload;
return {
...state,
contacts: state.contacts.map (item =>
item. _id === contact. _id? contact: item,
),
message: {
type: ‘success’,
title: ‘Update Successful’,
content: `Contact «${contact.email}» has been updated! `,
},
};
}
...
}
}
Далее преобразуем кнопку «РедактироватьContactCard «в компоненте в ссылку, которая будет направлять пользователя на форму:
// src/components/
...
import { Link } from ‘
const ContactCard = ({ contact }) => {
return (
...
<Button
basic
color="green"
as={Link}
to={`/contacts/edit/${contact. _id}`}
>
Edit
) ;
}
export default ContactCard;
Теперь, когда пользователи нажимают кнопку «Изменить «,
import React, { useContext, useEffect, useState } from ‘react’;
import axios from ‘axios’;
import ContactForm from '.../components/
import { flashErrorMessage } from '.../components/
import { ContactContext } from '.../context/
const ContactFormPage = ({ match }) => {
const [state, dispatch] = useContext (ContactContext) ;
const [loading, setLoading] = useState (true) ;
useEffect (() => {
const { _id } = match.params; // Grab URL _id
if (_id) {
const fetchData = async () => {
try {
const response = await axios.get (
`http: //localhost:3030/contacts/${_id}`,
) ;
dispatch ({
type: ‘FETCH_CONTACT’,
payload: response.data,
}) ;
setLoading (false) ;
} catch (error) {
flashErrorMessage (dispatch, error) ;
}
};
fetchData () ;
} else {
setLoading (false) ;
}
}, [match.params, dispatch]) ;
if (loading) {
return Please wait...;
}
return (
) ;
}
export default ContactFormPage;
Когда страница загружается, она проверяет, _idсуществует ли в
Мы также указали локальное loadingсостояние, которое установлено trueпо умолчанию. Это делается для того, чтобы отложить рендеринг ContactFormкомпонента до state.contactтех пор, пока он не будет заполнен. Чтобы понять, зачем нужна задержка, откройте src/components/
...
const ContactForm = ({contact}) => {
...
const { register, errors, handleSubmit } = useForm ({
defaultValues: contact,
}) ;
...
const updateContact = async data => {
try {
const response = await axios.patch (
`http: //localhost:3030/contacts/${contact. _id}`,
data,
) ;
dispatch ({
type: ‘UPDATE_CONTACT’,
payload: response.data,
}) ;
setRedirect (true) ;
} catch (error) {
flashErrorMessage (dispatch, error) ;
}
};
const onSubmit = async data => {
if (contact. _id) {
await updateContact (data) ;
} else {
await createContact (data) ;
}
};
...
return (
//... Display Form Mode
{contact. _id? «Edit Contact»: «Add New Contact»}
...
) ;
}
export default ContactForm;
Как вы можете видеть выше, мы ввели новую функцию для обновления контакта. Он почти идентичен createContact, за исключением того, что URL отличается, и мы используем
Возвращаясь к назначению loadingсостояния, как вы, наверное, знаете, React обычно перерисовывает, если данные, связанные с компонентом через свойства, изменяются. К сожалению, передать существующую contactформу React Hook можно только во время инициализации. Это означает, что при первой загрузке формы она пуста, так как fetchфункция асинхронная. К тому времени, когда он разрешится и заполнит state.contactполе, форма останется пустой, так как между ними нет связи.
Один из способов решить эту проблему — написать функцию, которая будет программно устанавливать значение каждого поля с помощью setValueфункции. Другой метод, который мы реализовали, состоит в том, чтобы просто отложить рендеринг ContactFormкомпонента до state.contactтех пор, пока он не будет заполнен.
Когда страница со списком завершит обновление, выберите любой контакт и нажмите кнопку «Изменить «.
Редактировать форму, отображающую существующий контакт
Завершите внесение изменений и нажмите «Сохранить».
Список отредактированных контактов
К настоящему времени ваше приложение должно позволять пользователям добавлять новые контакты и обновлять существующие.
Реализовать запрос на удаление
Давайте теперь посмотрим на последнюю операцию CRUD: удаление. Этот намного проще кодировать. Начнем с реализации DELETE_CONTACTредюсера в src/context/
function reducer (state, action) {
switch (action.type) {
...
case ‘DELETE_CONTACT’: {
const { _id, email } = action.payload;
return {
...state,
contacts: state.contacts.filter (item => item. _id≠= _id),
message: {
type: ‘success’,
title: ‘Delete Successful’,
content: `Contact «${email}» has been deleted! `,
},
};
}
...
}
}
Далее мы реализуем функцию, которая выполняет фактическое удаление. Мы сделаем это в src/components/
...
import axios from ‘axios’;
import { ContactContext } from '.../context/
import { flashErrorMessage } from '. /
const { useContext } = React;
const ContactCard = ({ contact }) => {
//
const [state, dispatch] = useContext (ContactContext) ;
const deleteContact = async id => {
try {
const response = await axios.delete (
`http: //localhost:3030/contacts/${id}`,
) ;
dispatch ({
type: ‘DELETE_CONTACT’,
payload: response.data,
}) ;
} catch (error) {
flashErrorMessage (dispatch, error) ;
}
};
return (
...
...
) ;
}
export default ContactCard;
Подождите, пока браузер обновится, затем попробуйте удалить один или несколько контактов. Кнопка удаления должна работать, как и ожидалось, с сообщением подтверждения, отображаемым вверху.
В качестве задачи попробуйте изменить onClickобработчик кнопки удаления, чтобы он запрашивал у пользователя подтверждение или отмену действия удаления.
Вывод
Теперь у нас есть готовое приложение, созданное с использованием React и Feathers, которое может выполнять действия CREATE, READи. Теперь, когда вы понимаете логику CRUD в приложении React, вы можете свободно заменять технологии. Например, вы можете использовать другую структуру CSS, такую как Bulma, Materialize или Bootstrap. Вы также можете использовать другой внутренний сервер, такой как LoopBack, или безголовую платформу CMS, такую как Strapi.UPDATEDELETE
Я также хотел бы отметить, что код, который мы написали, может быть улучшен многими способами. Например, мы можем:
заменить жестко закодированные
рефакторить код в определенных местах, чтобы сделать его чище
добавить документацию через комментарии
реализовать код редуктора в отдельном файле
создайте actionsфайл и поместите туда весь код, связанный с выборкой #
улучшить обработку ошибок, внедрив удобные для пользователя сообщения
писать модульные и сквозные тесты с использованием современных фреймворков тестирования
# Вы можете решить не делать этого и вместо этого поместить код действия рядом с тем, где он используется. Однако бывают ситуации, когда код действия может быть вызван более чем в одном месте. В этом случае рекомендуется переместить такой код в общий файл действий.
Если вы хотите узнать больше о том, как создавать лучшие приложения для управления информацией, я рекомендую вам изучить следующее:
ГрафQL
Next.js
GraphQL — это более новая технология, которая заменяет REST API. Это позволяет разработчикам внешнего интерфейса запрашивать объединенные записи. Вы не можете объединять записи с помощью REST API, если не напишете собственный маршрут, который выполняет запрос JOIN SQL/не SQL. Feathers поддерживает GraphQL через fgraphqlхук, так что вы можете легко начать использовать GraphQL в своем внешнем интерфейсе.
Next.js — это фреймворк для рендеринга сервера, который обеспечивает лучшую