Создание современного проекта требует разделения логики на внешний и внутренний код. Причиной этого шага является содействие повторному использованию кода. Например, нам может понадобиться создать собственное мобильное приложение, которое обращается к внутреннему API. Или мы можем разрабатывать модуль, который будет частью большой модульной платформы.
Популярный способ создания серверного API — это использование библиотек, таких как Express или Restify. Эти библиотеки облегчают создание маршрутов RESTful. Проблема с этими библиотеками заключается в том, что мы находим тонну повторяющегося кода . Нам также нужно будет написать код для авторизации и другой логики промежуточного программного обеспечения.
Чтобы избежать этой дилеммы, мы можем использовать такую среду, как Loopback или Feathers, чтобы помочь нам создать API.
На момент написания этой статьи у Loopback было больше звезд и загрузок с GitHub, чем у Feathers. Loopback — отличная библиотека для генерации конечных точек RESTful CRUD за короткий промежуток времени. Тем не менее, у него есть небольшая кривая обучения, и с документацией не легко ладить. Он имеет строгие требования к структуре. Например, все модели должны наследовать один из встроенных классов моделей. Если вам нужны возможности реального времени в Loopback, будьте готовы сделать дополнительное кодирование, чтобы оно заработало.
С FeathersJS, с другой стороны, гораздо легче начать работу и встроенную поддержку в реальном времени. Совсем недавно была выпущена версия Auk (поскольку Feathers настолько модульны, что используют названия птиц для названий версий), которая внесла огромное количество изменений и улучшений в ряде областей. Согласно сообщению, которое они опубликовали в своем блоге, сейчас они являются 4-м по популярности веб-фреймворком в реальном времени . Он имеет отличную документацию , и он охватывает практически любую область, в которой мы можем придумать создание API в реальном времени.
Что делает Перья удивительными, так это их простота. Вся структура является модульной, и нам нужно только установить необходимые функции. Сама Feathers — это тонкая оболочка, созданная поверх Express , куда они добавили новые функции — сервисы и хуки . Перья также позволяют нам легко отправлять и получать данные через WebSockets.
Предпосылки
Прежде чем приступить к работе с учебником, вам необходимо иметь прочную основу в следующих темах:
- Как написать код ES6 JavaScript
- Как создать компоненты React
- Неизменность в JavaScript
- Как управлять состоянием с Redux
На вашем компьютере вам необходимо установить последние версии:
- NodeJS 6+
- Mongodb 3.4+
- Менеджер пакетов пряжи (по желанию)
- Браузер Chrome
Если вы никогда ранее не писали API базы данных на JavaScript, я рекомендую сначала взглянуть на это руководство по созданию RESTful API .
Эшафот приложение
Мы собираемся создать приложение диспетчера контактов CRUD, используя React , Redux , Feathers и MongoDB . Вы можете посмотреть на завершенный проект здесь .
В этом уроке я покажу вам, как создать приложение снизу вверх. Мы запустим наш проект с помощью инструмента create-реагировать на приложение .
# scaffold a new react project create-react-app react-contact-manager cd react-contact-manager # delete unnecessary files rm src/logo.svg src/App.css
Используйте ваш любимый редактор кода и удалите все содержимое в index.css. Откройте App.js и перепишите код следующим образом:
import React, { Component } from 'react'; class App extends Component { render() { return ( <div> <h1>Contact Manager</h1> </div> ); } } export default App;
Убедитесь, что запускается запуск yarn start
чтобы убедиться, что проект работает, как ожидалось. Проверьте вкладку консоли, чтобы убедиться, что наш проект работает без каких-либо предупреждений или ошибок. Если все работает нормально, используйте Ctrl+C
чтобы остановить сервер.
Создайте сервер API с перьями
Давайте приступим к созданию внутреннего API для нашего CRUD-проекта с помощью инструмента feathers-cli
.
# Install Feathers command-line tool npm install -g feathers-cli # Create directory for the back-end code mkdir backend cd backend # Generate a feathers back-end API server feathers generate app ? 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 # Generate RESTful routes for Contact Model feathers generate service ? What kind of service is it? | Mongoose ? What is the name of the service? | contact ? Which path should the service be registered on? | /contacts ? What is the database connection string? | mongodb://localhost:27017/backend # Install email field type yarn add mongoose-type-email # Install the nodemon package yarn add nodemon --dev
Откройте backend/package.json
и обновите сценарий запуска, чтобы использовать nodemon, чтобы сервер API автоматически перезагружался при каждом внесении изменений.
// backend/package.json … "scripts": { ... "start": "nodemon src/", … }, …
Давайте откроем backend/config/default.json
. Здесь мы можем настроить параметры подключения MongoDB и другие параметры. Я также увеличил значение paginate по умолчанию до 50, поскольку в этом уроке мы не будем писать интерфейсную логику для разбивки на страницы.
{ "host": "localhost", "port": 3030, "public": "../public/", "paginate": { "default": 50, "max": 50 }, "mongodb": "mongodb://localhost:27017/backend" }
Откройте backend/src/models/contact.model.js
и обновите код следующим образом:
// backend/src/models/contact.model.js require('mongoose-type-email'); module.exports = function (app) { const mongooseClient = app.get('mongooseClient'); const contact = new mongooseClient.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!' } }, createdAt: { type: Date, 'default': Date.now }, updatedAt: { type: Date, 'default': Date.now } }); return mongooseClient.model('contact', contact); };
Помимо создания службы контактов, компания Feathers также создала для нас контрольный пример. Нам нужно исправить имя службы, чтобы оно прошло:
// backend/test/services/contact.test.js const assert = require('assert'); const app = require('../../src/app'); describe('\'contact\' service', () => { it('registered the service', () => { const service = app.service('contacts'); // change contact to contacts assert.ok(service, 'Registered the service'); }); });
Откройте новый терминал и в бэкэнд-каталоге выполните yarn test
. Вы должны успешно выполнить все тесты. Идите вперед и выполните yarn start
чтобы запустить внутренний сервер. Как только сервер завершит запуск, он должен напечатать строку: 'Feathers application started on localhost:3030'
.
Запустите браузер и получите доступ к URL: http: // localhost: 3030 / contacts . Вы должны ожидать получить следующий ответ JSON:
{"total":0,"limit":50,"skip":0,"data":[]}
Теперь давайте воспользуемся почтальоном, чтобы убедиться, что все маршруты CRUD работают. Вы можете запустить Почтальон, используя эту кнопку:
Если вы новичок в Почтальоне, ознакомьтесь с этим руководством . Когда вы нажмете кнопку «ОТПРАВИТЬ», вы должны получить обратно свои данные в качестве ответа вместе с тремя дополнительными полями — createdAt
, createdAt
и updatedAt
.
Используйте следующие данные JSON, чтобы сделать запрос POST с помощью Postman. Вставьте это в тело и установите тип содержимого для application/json
:
{ "name": { "first": "Tony", "last": "Stark" }, "phone": "+18138683770", "email": "[email protected]" }
Построить пользовательский интерфейс
Начнем с установки необходимых интерфейсных зависимостей. Мы будем использовать semantic-ui css / semantic-ui реагировать на стиль наших страниц и реагировать-маршрутизатор-dom для обработки навигации по маршруту.
Важно: убедитесь, что вы устанавливаете вне бэкэнд-каталога
// Install semantic-ui yarn add semantic-ui-css semantic-ui-react // Install react-router yarn add react-router-dom
Обновите структуру проекта, добавив следующие каталоги и файлы:
|-- react-contact-manager |-- backend |-- node_modules |-- public |-- src |-- App.js |-- App.test.js |-- index.css |-- index.js |-- components | |-- contact-form.js #(new) | |-- contact-list.js #(new) |-- pages |-- contact-form-page.js #(new) |-- contact-list-page.js #(new)
Давайте быстро заполним JS-файлы некоторым заполнителем.
Для компонента contact-list.js
мы напишем его в этом синтаксисе, так как это будет чисто презентационный компонент.
// src/components/contact-list.js import React from 'react'; export default function ContactList(){ return ( <div> <p>No contacts here</p> </div> ) }
Для контейнеров верхнего уровня я использую страницы. Давайте предоставим некоторый код для contact-list-page.js
// src/pages/contact-list-page.js import React, { Component} from 'react'; import ContactList from '../components/contact-list'; class ContactListPage extends Component { render() { return ( <div> <h1>List of Contacts</h1> <ContactList/> </div> ) } } export default ContactListPage;
Для компонента contact-form
он должен быть умным, поскольку он должен управлять своим собственным состоянием, в частности полями формы. Сейчас мы разместим этот код заполнителя.
// src/components/contact-form.js import React, { Component } from 'react'; class ContactForm extends Component { render() { return ( <div> <p>Form under construction</p> </div> ) } } export default ContactForm;
Заполните contact-form-page
этим кодом:
// src/pages/contact-form-page.js import React, { Component} from 'react'; import ContactForm from '../components/contact-form'; class ContactFormPage extends Component { render() { return ( <div> <ContactForm/> </div> ) } } export default ContactFormPage;
Теперь давайте создадим меню навигации и определим маршруты для нашего приложения. App.js
часто называют «шаблоном макета» для App.js
приложения.
// src/App.js import React, { Component } from 'react'; import { NavLink, Route } from 'react-router-dom'; import { Container } from 'semantic-ui-react'; import ContactListPage from './pages/contact-list-page'; import ContactFormPage from './pages/contact-form-page'; class App extends Component { render() { return ( <Container> <div className="ui two item menu"> <NavLink className="item" activeClassName="active" exact to="/"> Contacts List </NavLink> <NavLink className="item" activeClassName="active" exact to="/contacts/new"> Add Contact </NavLink> </div> <Route exact path="/" component={ContactListPage}/> <Route path="/contacts/new" component={ContactFormPage}/> <Route path="/contacts/edit/:_id" component={ContactFormPage}/> </Container> ); } } export default App;
Наконец, обновите файл index.js
этим кодом, в который мы импортируем semantic-ui CSS для стилей и BrowserRouter для использования API истории HTML5, который синхронизирует наше приложение с URL-адресом.
// src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; import 'semantic-ui-css/semantic.min.css'; import './index.css'; ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') );
Вернитесь к терминалу и выполните yarn start
. Вы должны иметь похожий вид на скриншот ниже:
Управление состоянием реагирования с помощью Redux
Остановите сервер с помощью ctrl+c
и установите следующие пакеты с помощью менеджера пакетов yarn:
yarn add redux react-redux redux-promise-middleware redux-thunk redux-devtools-extension axios
Уф! Это целая куча пакетов для настройки Redux. Я предполагаю, что вы уже знакомы с Redux, если вы читаете этот учебник. Redux-thunk позволяет создавать создателей действий как асинхронные функции, в то время как redux-обещание-промежуточное ПО уменьшает для нас некоторый шаблонный код Redux, обрабатывая отправку ожидающих, выполненных и отклоненных действий от нашего имени.
Feathers включает в себя легкий клиентский пакет, который помогает общаться с API, но также очень легко использовать другие клиентские пакеты. Для этого урока мы будем использовать HTTP-клиент Axios .
Reduce-devtools-extension — удивительный инструмент, который отслеживает отправленные действия и изменения состояния. Вам нужно будет установить расширение Chrome, чтобы оно работало.
Далее, давайте настроим нашу структуру каталогов Redux следующим образом:
|-- react-contact-manager |-- backend |-- node_modules |-- public |-- src |-- App.js |-- App.test.js |-- index.css |-- index.js |-- contact-data.js #new |-- store.js #new |-- actions #new |-- contact-actions.js #new |-- index.js #new |-- components |-- pages |-- reducers #new |-- contact-reducer.js #new |-- index.js #new
Давайте начнем с contacts-data.js
некоторыми тестовыми данными:
// src/contact-data.js export const contacts = [ { _id: "1", name: { first:"John", last:"Doe" }, phone:"555", email:"[email protected]" }, { _id: "2", name: { first:"Bruce", last:"Wayne" }, phone:"777", email:"[email protected]" } ];
по// src/contact-data.js export const contacts = [ { _id: "1", name: { first:"John", last:"Doe" }, phone:"555", email:"[email protected]" }, { _id: "2", name: { first:"Bruce", last:"Wayne" }, phone:"777", email:"[email protected]" } ];
по// src/contact-data.js export const contacts = [ { _id: "1", name: { first:"John", last:"Doe" }, phone:"555", email:"[email protected]" }, { _id: "2", name: { first:"Bruce", last:"Wayne" }, phone:"777", email:"[email protected]" } ];
Определите contact-actions.js
с помощью следующего кода. Сейчас мы будем получать данные из файла contacts-data.js
.
// src/actions/contact-actions.js import { contacts } from '../contacts-data'; export function fetchContacts(){ return dispatch => { dispatch({ type: 'FETCH_CONTACTS', payload: contacts }) } }
В contact-reducer.js
напишем наш обработчик для действия 'FETCH_CONTACT'
. Мы будем хранить данные о контактах в массиве, называемом 'contacts'
.
// src/reducers/contact-reducer.js const defaultState = { contacts: [] } export default (state=defaultState, action={}) => { switch (action.type) { case 'FETCH_CONTACTS': { return { ...state, contacts: action.payload } } default: return state; } }
В reducers/index.js
мы объединяем здесь все редукторы для простого экспорта в наш магазин Redux.
// src/reducers/index.js import { combineReducers } from 'redux'; import ContactReducer from './contact-reducer'; const reducers = { contactStore: ContactReducer } const rootReducer = combineReducers(reducers); export default rootReducer;
В store.js
мы импортируем необходимые зависимости для создания нашего хранилища Redux. Мы также redux-devtools-extension
здесь, чтобы мы могли отслеживать хранилище Redux с помощью расширения Chrome .
// src/store.js import { applyMiddleware, createStore } from "redux"; import thunk from "redux-thunk"; import promise from "redux-promise-middleware"; import { composeWithDevTools } from 'redux-devtools-extension'; import rootReducer from "./reducers"; const middleware = composeWithDevTools(applyMiddleware(promise(), thunk)); export default createStore(rootReducer, middleware);
Откройте index.js
и обновите метод рендеринга, где мы добавляем хранилище, используя класс провайдера Redux.
// src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import App from './App'; import store from "./store" import 'semantic-ui-css/semantic.min.css'; import './index.css'; ReactDOM.render( <BrowserRouter> <Provider store={store}> <App /> </Provider> </BrowserRouter>, document.getElementById('root') );
Давайте запустим yarn start
чтобы убедиться, что все работает.
Далее мы свяжем наш contact-list
компонента contact-list
только что созданным магазином Redux. Откройте contact-list-page
и обновите код следующим образом:
// src/pages/contact-list-page import React, { Component} from 'react'; import { connect } from 'react-redux'; import ContactList from '../components/contact-list'; import { fetchContacts } from '../actions/contact-actions'; class ContactListPage extends Component { componentDidMount() { this.props.fetchContacts(); } render() { return ( <div> <h1>List of Contacts</h1> <ContactList contacts={this.props.contacts}/> </div> ) } } // Make contacts array available in props function mapStateToProps(state) { return { contacts : state.contactStore.contacts } } export default connect(mapStateToProps, {fetchContacts})(ContactListPage);
Мы сделали массив контактов в хранилище и функцию fetchContacts
доступной для компонента ContactListPage
через переменную this.props
. Теперь мы можем передать массив контактов в компонент ContactList
.
А пока давайте обновим код, чтобы мы могли отобразить список контактов.
// src/components/contact-list import React from 'react'; export default function ContactList({contacts}){ const list = () => { return contacts.map(contact => { return ( <li key={contact._id}>{contact.name.first} {contact.name.last}</li> ) }) } return ( <div> <ul> { list() } </ul> </div> ) }
Если вы вернетесь в браузер, у вас должно получиться что-то вроде этого:
Давайте сделаем интерфейс списка более привлекательным, используя компонент Card семантического интерфейса. В папке компонентов создайте новый файл contact-card.js
и вставьте этот код:
// src/components/contact-card.js import React from 'react'; import { Card, Button, Icon } from 'semantic-ui-react' export default function ContactCard({contact, deleteContact}) { return ( <Card> <Card.Content> <Card.Header> <Icon name='user outline'/> {contact.name.first} {contact.name.last} </Card.Header> <Card.Description> <p><Icon name='phone'/> {contact.phone}</p> <p><Icon name='mail outline'/> {contact.email}</p> </Card.Description> </Card.Content> <Card.Content extra> <div className="ui two buttons"> <Button basic color="green">Edit</Button> <Button basic color="red">Delete</Button> </div> </Card.Content> </Card> ) } ContactCard.propTypes = { contact: React.PropTypes.object.isRequired }
Обновите компонент contact-list
чтобы использовать новый компонент ContactCard
// src/components/contact-list.js import React from 'react'; import { Card } from 'semantic-ui-react'; import ContactCard from './contact-card'; export default function ContactList({contacts}){ const cards = () => { return contacts.map(contact => { return ( <ContactCard key={contact._id} contact={contact}/> ) }) } return ( <Card.Group> { cards() } </Card.Group> ) }
Страница списка теперь должна выглядеть так:
Проверка на стороне сервера с Redux-формой
Теперь, когда мы знаем, что хранилище Redux правильно связано с компонентами React, мы можем теперь сделать реальный запрос на выборку к базе данных и использовать эти данные для заполнения нашей страницы списка контактов. Есть несколько способов сделать это, но способ, который я покажу, на удивление прост.
Во-первых, нам нужно настроить клиент Axios, который может подключаться к внутреннему серверу.
// src/actions/index.js import axios from "axios"; export const client = axios.create({ baseURL: "http://localhost:3030", headers: { "Content-Type": "application/json" } })
Далее мы обновим код contact-actions.js
чтобы получать контакты из базы данных с помощью запроса GET с помощью клиента Axios.
// src/actions/contact-actions.js import { client } from './'; const url = '/contacts'; export function fetchContacts(){ return dispatch => { dispatch({ type: 'FETCH_CONTACTS', payload: client.get(url) }) } }
Также обновите contact-reducer.js
поскольку contact-reducer.js
действие и полезная нагрузка теперь отличаются.
// src/reducers/contact-reducer.js … case "FETCH_CONTACTS_FULFILLED": { return { ...state, contacts: action.payload.data.data || action.payload.data // in case pagination is disabled } } …
После сохранения обновите браузер и убедитесь, что внутренний сервер работает на localhost:3030
. На странице списка контактов теперь должны отображаться данные из базы данных.
Обработка запросов на создание и обновление с помощью Redux-Form
Далее давайте посмотрим, как добавлять новые контакты, и для этого нам нужны формы. Во-первых, создание формы выглядит довольно просто. Но когда мы начинаем думать о проверке на стороне клиента и контроле, когда должны отображаться ошибки, это становится сложно. Кроме того, внутренний сервер выполняет свою собственную проверку, которая также необходима для отображения его ошибок в форме.
Вместо того, чтобы реализовывать всю функциональность формы самостоятельно, мы заручимся поддержкой библиотеки под названием Redux-Form . Мы также будем использовать отличный пакет под названием Classnames , который поможет нам выделить поля с ошибками проверки.
Нам нужно остановить сервер с помощью ctrl+c
перед установкой следующих пакетов:
yarn add redux-form classnames
Теперь мы можем запустить сервер после завершения установки пакетов.
Давайте сначала быстро добавим этот класс css в файл index.css
для index.css
ошибок формы:
/* src/index.css */ .error { color: #9f3a38; }
Тогда давайте добавим редуктор combineReducers
-form к функции combineReducers
в reducers/index.js
// src/reducers/index.js … import { reducer as formReducer } from 'redux-form'; const reducers = { contactStore: ContactReducer, form: formReducer } …
Затем откройте contact-form.js
и создайте пользовательский интерфейс формы с этим кодом:
// src/components/contact-form import React, { Component } from 'react'; import { Form, Grid, Button } from 'semantic-ui-react'; import { Field, reduxForm } from 'redux-form'; import classnames from 'classnames'; class ContactForm extends Component { renderField = ({ input, label, type, meta: { touched, error } }) => ( <Form.Field className={classnames({error:touched && error})}> <label>{label}</label> <input {...input} placeholder={label} type={type}/> {touched && error && <span className="error">{error.message}</span>} </Form.Field> ) render() { const { handleSubmit, pristine, submitting, loading } = this.props; return ( <Grid centered columns={2}> <Grid.Column> <h1 style={{marginTop:"1em"}}>Add New Contact</h1> <Form onSubmit={handleSubmit} loading={loading}> <Form.Group widths='equal'> <Field name="name.first" type="text" component={this.renderField} label="First Name"/> <Field name="name.last" type="text" component={this.renderField} label="Last Name"/> </Form.Group> <Field name="phone" type="text" component={this.renderField} label="Phone"/> <Field name="email" type="text" component={this.renderField} label="Email"/> <Button primary type='submit' disabled={pristine || submitting}>Save</Button> </Form> </Grid.Column> </Grid> ) } } export default reduxForm({form: 'contact'})(ContactForm);
Потратьте время, чтобы изучить код; там много чего происходит. Обратитесь к справочному руководству, чтобы понять, как работает избыточная форма Кроме того, взгляните на документацию по семантическому интерфейсу и прочитайте о ее элементах, чтобы понять, как они используются в этом контексте.
Далее мы определим действия, необходимые для добавления нового контакта в базу данных. Первое действие предоставит новый contact
объект в форму Redux. Пока второе действие опубликует contact
данные на сервере API.
Добавьте следующий код в contact-actions.js
// src/actions/contact-actions.js … export function newContact() { return dispatch => { dispatch({ type: 'NEW_CONTACT' }) } } export function saveContact(contact) { return dispatch => { return dispatch({ type: 'SAVE_CONTACT', payload: client.post(url, contact) }) } }
В contact-reducer
нам нужно обработать действия для 'NEW_CONTACT'
, 'SAVE_CONTACT_PENDING'
, 'SAVE_CONTACT_FULFILLED'
и 'SAVE_CONTACT_REJECTED'
. Нам нужно объявить следующие переменные:
- contact — инициализировать пустой объект
- loading — обновить интерфейс с информацией о прогрессе
- ошибки — хранить ошибки проверки сервера на случай, если что-то пойдет не так
Добавьте этот код в оператор переключения contact-reducer
:
// src/reducers/contact-reducer.js … const defaultState = { contacts: [], contact: {name:{}}, loading: false, errors: {} } … case 'NEW_CONTACT': { return { ...state, contact: {name:{}} } } case 'SAVE_CONTACT_PENDING': { return { ...state, loading: true } } case 'SAVE_CONTACT_FULFILLED': { return { ...state, contacts: [...state.contacts, action.payload.data], errors: {}, loading: false } } case 'SAVE_CONTACT_REJECTED': { const data = action.payload.response.data; // convert feathers error formatting to match client-side error formatting const { "name.first":first, "name.last":last, phone, email } = data.errors; const errors = { global: data.message, name: { first,last }, phone, email }; return { ...state, errors: errors, loading: false } } …
Откройте contact-form-page.js
и обновите код следующим образом:
// src/pages/contact-form-page import React, { Component} from 'react'; import { Redirect } from 'react-router'; import { SubmissionError } from 'redux-form'; import { connect } from 'react-redux'; import { newContact, saveContact } from '../actions/contact-actions'; import ContactForm from '../components/contact-form'; class ContactFormPage extends Component { state = { redirect: false } componentDidMount() { this.props.newContact(); } submit = (contact) => { return this.props.saveContact(contact) .then(response => this.setState({ redirect:true })) .catch(err => { throw new SubmissionError(this.props.errors) }) } render() { return ( <div> { this.state.redirect ? <Redirect to="/" /> : <ContactForm contact={this.props.contact} loading={this.props.loading} onSubmit={this.submit} /> } </div> ) } } function mapStateToProps(state) { return { contact: state.contactStore.contact, errors: state.contactStore.errors } } export default connect(mapStateToProps, {newContact, saveContact})(ContactFormPage);
Давайте теперь вернемся к браузеру и попытаемся преднамеренно сохранить неполную форму
Как видите, проверка на стороне сервера не позволяет нам сохранить неполный контакт. Мы используем класс SubmissionError для передачи this.props.errors
в форму, на случай, если вам интересно.
Теперь закончите заполнение формы полностью. После нажатия кнопки сохранения мы должны быть направлены на страницу списка.
Проверка на стороне клиента с помощью формы Redux
Давайте посмотрим, как можно реализовать проверку на стороне клиента. Откройте contact-form
и вставьте этот код вне класса ContactForm. Также обновите экспорт по умолчанию, как показано:
// src/components/contact-form.js … const validate = (values) => { const errors = {name:{}}; if(!values.name || !values.name.first) { errors.name.first = { message: 'You need to provide First Name' } } if(!values.phone) { errors.phone = { message: 'You need to provide a Phone number' } } else if(!/^\+(?:[0-9] ?){6,14}[0-9]$/.test(values.phone)) { errors.phone = { message: 'Phone number must be in International format' } } if(!values.email) { errors.email = { message: 'You need to provide an Email address' } } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[AZ]{2,4}$/i.test(values.email)) { errors.email = { message: 'Invalid email address' } } return errors; } … export default reduxForm({form: 'contact', validate})(ContactForm);
После сохранения файла вернитесь в браузер и попробуйте добавить неверные данные. На этот раз проверка на стороне клиента блокирует отправку данных на сервер.
Теперь, вперёд, введите правильные данные. У нас должно быть как минимум три новых контакта.
Реализация обновлений контактов
Теперь, когда мы можем добавлять новые контакты, давайте посмотрим, как мы можем обновить существующие контакты. Мы начнем с файла contact-actions.js
, где нам нужно определить два действия — одно для извлечения одного контакта, а другое для обновления контакта.
// src/actions/contact-actions.js … export function fetchContact(_id) { return dispatch => { return dispatch({ type: 'FETCH_CONTACT', payload: client.get(`${url}/${_id}`) }) } } export function updateContact(contact) { return dispatch => { return dispatch({ type: 'UPDATE_CONTACT', payload: client.put(`${url}/${contact._id}`, contact) }) } }
Давайте добавим следующие случаи в contact-reducer
чтобы обновить состояние, когда контакт выбирается из базы данных и когда он обновляется.
// src/reducers/contact-reducer.js … case 'FETCH_CONTACT_PENDING': { return { ...state, loading: true, contact: {name:{}} } } case 'FETCH_CONTACT_FULFILLED': { return { ...state, contact: action.payload.data, errors: {}, loading: false } } case 'UPDATE_CONTACT_PENDING': { return { ...state, loading: true } } case 'UPDATE_CONTACT_FULFILLED': { const contact = action.payload.data; return { ...state, contacts: state.contacts.map(item => item._id === contact._id ? contact : item), errors: {}, loading: false } } case 'UPDATE_CONTACT_REJECTED': { const data = action.payload.response.data; const { "name.first":first, "name.last":last, phone, email } = data.errors; const errors = { global: data.message, name: { first,last }, phone, email }; return { ...state, errors: errors, loading: false } } …
Далее давайте передадим новую выборку и сохраняем действия в contact-form-page.js
. Мы также изменим логику componentDidMount()
и submit()
для обработки сценариев создания и обновления. Обязательно обновите каждый раздел кода, как указано ниже.
// src/pages/contact-form-page.js … import { newContact, saveContact, fetchContact, updateContact } from '../actions/contact-actions'; … componentDidMount = () => { const { _id } = this.props.match.params; if(_id){ this.props.fetchContact(_id) } else { this.props.newContact(); } } submit = (contact) => { if(!contact._id) { return this.props.saveContact(contact) .then(response => this.setState({ redirect:true })) .catch(err => { throw new SubmissionError(this.props.errors) }) } else { return this.props.updateContact(contact) .then(response => this.setState({ redirect:true })) .catch(err => { throw new SubmissionError(this.props.errors) }) } } … export default connect( mapStateToProps, {newContact, saveContact, fetchContact, updateContact})(ContactFormPage);
Мы fetchContact()
contact-form
для асинхронного получения данных от действия fetchContact()
. Чтобы заполнить форму Redux, мы используем ее функцию инициализации, которая стала доступна нам через props
. Мы также обновим заголовок страницы с помощью скрипта, чтобы отразить, редактируем ли мы или добавляем новый контакт.
// src/components/contact-form.js … componentWillReceiveProps = (nextProps) => { // Receive Contact data Asynchronously const { contact } = nextProps; if(contact._id !== this.props.contact._id) { // Initialize form only once this.props.initialize(contact) } } … <h1 style={{marginTop:"1em"}}>{this.props.contact._id ? 'Edit Contact' : 'Add New Contact'}</h1> …
Теперь давайте преобразуем кнопку « Изменить» в contact-card.js
в ссылку, которая направит пользователя в форму.
// src/components/contact-card.js … import { Link } from 'react-router-dom'; … <div className="ui two buttons"> <Link to={`/contacts/edit/${contact._id}`} className="ui basic button green">Edit</Link> <Button basic color="red">Delete</Button> </div> …
После обновления страницы списка выберите любой контакт и нажмите кнопку «Изменить».
Завершите внесение изменений и нажмите «Сохранить».
К настоящему времени ваше приложение должно иметь возможность разрешать пользователям добавлять новые контакты и обновлять существующие.
Реализовать запрос на удаление
Давайте теперь посмотрим на финальную операцию CRUD: удалить. Этот намного проще для кода. Начнем contact-actions.js
файла contact-actions.js
.
// src/actions/contact-actions.js … export function deleteContact(_id) { return dispatch => { return dispatch({ type: 'DELETE_CONTACT', payload: client.delete(`${url}/${_id}`) }) } }
К настоящему времени вы должны были получить тренировку. Определите регистр для действия deleteContact()
в contact-reducer.js
.
// src/reducers/contact-reducer.js … case 'DELETE_CONTACT_FULFILLED': { const _id = action.payload.data._id; return { ...state, contacts: state.contacts.filter(item => item._id !== _id) } } …
Затем мы импортируем действие deleteContact()
в contact-list-page.js
и передаем его компоненту ContactList
.
// src/pages/contact-list-page.js … import { fetchContacts, deleteContact } from '../actions/contact-actions'; … <ContactList contacts={this.props.contacts} deleteContact={this.props.deleteContact}/> … export default connect(mapStateToProps, {fetchContacts, deleteContact})(ContactListPage);
Компонент ContactList
, в свою очередь, передает действие deleteContact()
компоненту ContactCard
// src/components/contact-list.js … export default function ContactList({contacts, deleteContact}){ // replace this line const cards = () => { return contacts.map(contact => { return ( <ContactCard key={contact._id} contact={contact} deleteContact={deleteContact} /> // and this one ) }) } …
Наконец, мы обновляем кнопку Delete в ContactCard
чтобы выполнить действие deleteContact()
через атрибут onClick
.
// src/components/contact-card.js … <Button basic color="red" onClick={() => deleteContact(contact._id)} >Delete</Button> …
Подождите, пока браузер обновится, затем попытайтесь удалить один или несколько контактов. Кнопка удаления должна работать как положено.
В качестве проблемы попробуйте изменить обработчик onClick
кнопки удаления, чтобы он запрашивал у пользователя подтверждение или отмену действия удаления. Вставьте свое решение в комментариях ниже.
Вывод
К настоящему времени вы должны были изучить основы создания веб-приложения CRUD на JavaScript. Может показаться, что мы написали довольно много кода для управления только одной моделью. Мы могли бы сделать меньше работы, если бы использовали инфраструктуру MVC. Проблема этих структур заключается в том, что их становится все труднее поддерживать по мере роста кода.
Основанная на Flux среда, такая как Redux, позволяет нам создавать большие сложные проекты, которыми легко управлять. Если вам не нравится подробный код, который Redux требует от вас написать, вы можете также рассмотреть Mobx как альтернативу.
По крайней мере, я надеюсь, что у вас сейчас хорошее впечатление о FeathersJS. Без особых усилий мы смогли сгенерировать API базы данных, используя всего несколько команд и немного кода. Хотя мы изучили только его возможности, вы по крайней мере согласитесь со мной, что это надежное решение для создания API.
Эта статья была рецензирована Маршаллом Томпсоном и Себастьяном Зейтцем . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!