Статьи

Использование REST API с помощью React.js

В последнее время мы наблюдаем быстрый рост мобильной интернет-связи. У нас есть смартфоны, планшеты, нетбуки и другие подключенные устройства, которые создают потребность обслуживать соответствующий контент для каждого конкретного внешнего интерфейса. Также Интернет становится доступным для новых регионов и социальных групп, постоянно увеличивая интернет-трафик. С другой стороны, компьютеры пользователей и браузеры, а также сам JavaScript становятся все более и более мощными, предоставляя больше возможностей для обработки данных в Интернете через клиентскую часть. В этой ситуации лучшим решением часто может быть отправка данных в качестве ответа, а не отправка содержимого страницы. То есть нам не нужно перезагружать всю страницу при каждом запросе, а отправлять обратно соответствующие данные и позволить клиенту (интерфейсному компоненту) обрабатывать данные.

Мы можем разработать внутреннее приложение, предоставляющее удаленный API (обычно на основе протокола REST) ​​и внешнее (обычно JavaScript) приложение, которое взаимодействует с API и отображает все данные на устройстве.

Если данные бэкэнда потребляются людьми, нам необходимо разработать пользовательский интерфейс (UI), чтобы предоставить пользователям возможность управлять данными. Современные веб-приложения должны иметь отзывчивые и дружественные пользовательские интерфейсы, обеспечивающие адекватный пользовательский опыт. Кроме того, современные пользовательские интерфейсы могут быть сколь угодно сложными, с несколькими панелями, вложенными макетами, разбиением на страницы, индикаторами выполнения и т. Д. В этом случае компонентная модель может быть правильным решением. React.js — это облегченный JavaScript-фреймворк, ориентированный на создание веб-интерфейсов на основе компонентов. React не предоставляет никаких средств для связи с бэкэндом, но мы можем использовать любую коммуникационную библиотеку из компонентов React.

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

Перед началом разработки нам необходимо настроить среду разработки React.js.

1. Настройка среды разработки React.js

Есть несколько способов использовать React.js. Самый простой способ — просто включить библиотеки React в теги <script> на странице. 

Листинг 1.1. Включение библиотеки React.js на HTML-странице:

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <div id="hello_container" class=""></div>
    <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
    <script>
      class Hello extends React.Component {

        constructor(props) {
          super(props);
        }

        render() {
          return React.createElement(
            'div',
            null,
            `Hello ${this.props.name}!`
          );
        }
      }

      ReactDOM.render(React.createElement(Hello, {name: 'React'}, null), document.querySelector('#hello_container'));
    </script>
  </body>
</html>

Таким образом, мы можем очень быстро начать разработку приложений React, но мы не можем использовать некоторые расширенные функции, такие как, например, JSX . Таким образом, более подходящим решением, особенно для больших и сложных приложений, было бы использование   create-react-app инструмента. Для его установки на вашем компьютере должны быть установлены Node.js и npmnpm install -g create-react-app 

Затем вы можете запустить следующую команду в корневом каталоге, где вы хотите создать свой проект:

 .../project-root>npx create-react-app consuming-rest

Эта команда создает новую папку (‘consuming-rest’) с готовым к запуску прототипом приложения React.

Теперь мы можем войти в каталог и запустить приложение следующим образом:

 .../project-root>cd consuming-rest

.../project-root/consuming-rest>npm start

 Это запустит приложение в новом браузере по адресу http: // localhost: 3000 :

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

Изначально мы можем реализовать службу данных для связи с сервером.

2. Внедрение услуг бэкэнд-связи

В целом, это хорошая идея, чтобы поместить все связанные функции в одном месте. Использование нашей функциональности в сервисе, который предоставляет определенные API, обеспечивает большую гибкость и тестируемость для нашего приложения. Итак, мы создаем класс коммуникационного сервиса, который реализует все основные операции CRUD для обмена данными с сервером и предоставляет эти операции в качестве методов для всех компонентов React. Чтобы сделать наш интерфейс более отзывчивым, мы реализуем методы как асинхронные. При условии неизменного API, мы можем свободно изменять реализацию, и ни один из потребителей не будет затронут. Чтобы реализовать эти концепции на практике, давайте создадим фиктивную реализацию службы, которая предоставляет фиктивные данные для построения и тестирования нашего пользовательского интерфейса. Наш фиктивный сервис может выглядеть так.

Листинг 2.1. src / shared / mock-item-service, js — mock ItemService:

class ItemService {

  constructor() {
    this.items = [
      {link:1, name:"test1", summary:"Summary Test 1", year:"2001", country:"us", price:"1000", description:"Desc 1"},
      {link:2, name:"test2", summary:"Summary Test 2", year:"2002", country:"uk", price:"2000", description:"Desc 2"},
      {link:3, name:"test3", summary:"Summary Test 3", year:"2003", country:"cz", price:"3000", description:"Desc 3"},
    ];
  }

  async retrieveItems() {
      return Promise.resolve(this.items);
  }

  async getItem(itemLink) {
    for(var i = 0; i < this.items.length; i++) {
      if ( this.items[i].link === itemLink) {
        return Promise.resolve(this.items[i]);
      }
    }
    return null;
  }

  async createItem(item) {
    console.log("ItemService.createItem():");
    console.log(item);
    return Promise.resolve(item);
  }

  async deleteItem(itemId) {
    console.log("ItemService.deleteItem():");
    console.log("item ID:" + itemId);
  }

  async updateItem(item) {
    console.log("ItemService.updateItem():");
    console.log(item);
  }

}

export default ItemService;

На основании этого мы можем построить пользовательский интерфейс.

3. Внедрение CRUD UI

React поддерживает иерархии компонентов, где каждый компонент может иметь состояние, а состояние может быть общим для связанных компонентов. Кроме того, поведение каждого компонента можно настроить, передав ему свойства. Таким образом, мы можем разработать основной компонент, который содержит список элементов коллекции и работает как заполнитель для отображения форм для соответствующих действий CRUD. Используя материал, сгенерированный инструментом create-реагировать на приложение, мы меняем содержимое app.js следующим образом.

Листинг 3.1. src / App.js — основной компонент в качестве фрейма приложения:

import React, { Component } from 'react';
import './App.css';
import ItemDetails from './item-details';
import NewItem from './new-item';
import EditItem from './edit-item';
import ItemService from './shared/mock-item-service';

class App extends Component {

  constructor(props) {
    super(props);
    this.itemService = new ItemService();
    this.onSelect = this.onSelect.bind(this);
    this.onNewItem = this.onNewItem.bind(this);
    this.onEditItem = this.onEditItem.bind(this);
    this.onCancel = this.onCancel.bind(this);
    this.onCancelEdit = this.onCancelEdit.bind(this);
    this.onCreateItem = this.onCreateItem.bind(this);
    this.onUpdateItem = this.onUpdateItem.bind(this);
    this.onDeleteItem = this.onDeleteItem.bind(this);
    this.state = {
      showDetails: false,
      editItem: false,
      selectedItem: null,
      newItem: null
    }
  }

  componentDidMount() {
      this.getItems();
  }

  render() {
    const items = this.state.items;
    if(!items) return null;
    const showDetails = this.state.showDetails;
    const selectedItem = this.state.selectedItem;
    const newItem = this.state.newItem;
    const editItem = this.state.editItem;
    const listItems = items.map((item) =>
      <li key={item.link} onClick={() => this.onSelect(item.link)}>
         <span className="item-name">{item.name}</span>&nbsp;|&nbsp; {item.summary}
      </li>
    );

    return (
      <div className="App">
          <ul className="items">
            {listItems}
          </ul>
          <br/>
          <button type="button" name="button" onClick={() => this.onNewItem()}>New Item</button>
          <br/>
            {newItem && <NewItem onSubmit={this.onCreateItem} onCancel={this.onCancel}/>}
            {showDetails && selectedItem && <ItemDetails item={selectedItem} onEdit={this.onEditItem}  onDelete={this.onDeleteItem} />}
            {editItem && selectedItem && <EditItem onSubmit={this.onUpdateItem} onCancel={this.onCancelEdit} item={selectedItem} />}
      </div>
    );
  }

  getItems() {
    this.itemService.retrieveItems().then(items => {
          this.setState({items: items});
        }
    );
  }

  onSelect(itemLink) {
    this.clearState();
    this.itemService.getItem(itemLink).then(item => {
      this.setState({
          showDetails: true,
          selectedItem: item
        });
      }
    );
  }

  onCancel() {
    this.clearState();
  }

  onNewItem() {
    this.clearState();
    this.setState({
      newItem: true
    });
  }

  onEditItem() {
    this.setState({
      showDetails: false,
      editItem: true,
      newItem: null
    });
  }

  onCancelEdit() {
    this.setState({
      showDetails: true,
      editItem: false,
      newItem: null
    });
  }

  onUpdateItem(item) {
    this.clearState();
    this.itemService.updateItem(item).then(item => {
        this.getItems();
      }
    );
  }

  onCreateItem(newItem) {
    this.clearState();
    this.itemService.createItem(newItem).then(item => {
        this.getItems();
      }
    );
  }

  onDeleteItem(itemLink) {
    this.clearState();
    this.itemService.deleteItem(itemLink).then(res => {
        this.getItems();
      }
    );
  }

  clearState() {
    this.setState({
      showDetails: false,
      selectedItem: null,
      editItem: false,
      newItem: null
    });
  }
}

export default App;

Обратите внимание, что на данный момент наш компонент приложения использует фиктивный сервис, который мы создали в разделе 2: 

, , ,

import ItemService from './shared/mock-item-service';

, , ,

Затем мы создадим вложенные компоненты для основных операций с элементами коллекции.

Листинг 3.2. src / new-item.js — создание новых элементов коллекции:

import React, { Component } from 'react';
import './App.css';
import Validator from './shared/validator';

class NewItem extends Component {

  constructor(props) {
    super(props);
    this.validator = new Validator();
    this.onCancel = this.onCancel.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
    this.state = {
      name: '',
      summary: '',
      year: '',
      country: '',
      description: ''
    };
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  onCancel() {
    this.props.onCancel();
  }

  onSubmit() {
    if(this.validator.validateInputs(this.state)) {
        this.props.onSubmit(this.state);
    }
  }

  render() {
    return (
      <div className="input-panel">
      <span className="form-caption">New item:</span>
      <div>
        <label className="field-name">Name:<br/>
          <input value={this.state.name} name="name" maxLength="40" required onChange={this.handleInputChange} placeholder="item name" />
        </label>
      </div>
      <div>
        <label className="field-name">Summary:<br/>
          <input value={this.state.summary} name="summary" maxLength="40" required onChange={this.handleInputChange} placeholder="summary" />
        </label>
      </div>
      <div>
        <label className="field-name">Year:<br/>
          <input value={this.state.year} name="year" maxLength="4" pattern="[0-9]{1,4}" onChange={this.handleInputChange} placeholder="year" />
        </label>
      </div>
      <div>
        <label className="field-name">Country:<br/>
          <input value={this.state.country} name="country" maxLength="2" pattern="[a-z|A-Z]{2}" onChange={this.handleInputChange} placeholder="country code" />
        </label>
      </div>
      <div>
        <label className="field-name">Description:<br/>
          <textarea value={this.state.description} name="description" onChange={this.handleInputChange} placeholder="description" />
        </label>
      </div>
      <br/>
      <button onClick={() => this.onCancel()}>Cancel</button>&nbsp;
      <button onClick={() => this.onSubmit()}>Create</button>
      </div>
    );
  }
}

export default NewItem;

В листинге 3.2 мы используем  validator класс, который обеспечивает простую проверку для вновь созданных или отредактированных элементов коллекции. Этот класс может быть разделен между компонентами, т.е. он может быть использован в  NewItem и  EditItem компоненты в нашем случае.

Листинг 3.3. src / shared / validatior.js — простая проверка формы элемента:

class Validator {

  validateInputs(inputData) {
    let errorMsg = "";
    if(!inputData.name) {
      errorMsg +="Please enter name of this item.\n"
    }
    if(!inputData.summary) {
      errorMsg +="Please enter summary of this item.\n"
    }
    if(inputData.year.toString().match(/[^0-9]/g)) {
      errorMsg +="Year must be a number.\n"
    }
    if(inputData.country.length > 0 && !inputData.country.match(/^[a-z|A-Z][a-z|A-Z]$/)) {
      errorMsg +="Country code must be two letters.\n"
    }
    if(errorMsg.length == 0){
      return true;
    } else {
      alert(errorMsg);
      return false;
    }
  }
}

export default Validator;

Листинг 3.4. src / item-details.js — просмотр сведений об элементе:

import React, { Component } from 'react';
import './App.css';

class ItemDetails extends Component {

  constructor(props) {
    super(props);
    this.onEdit = this.onEdit.bind(this);
    this.onDelete = this.onDelete.bind(this);
  }

  render() {
    const item = this.props.item;
    return (
      <div className="input-panel">
      <span className="form-caption">{ item.name}</span>
      <div><span className="field-name">Name:</span><br/> {item.name}</div>
      <div><span className="field-name">Summary:</span><br/> {item.summary}</div>
      <div><span className="field-name">Year:</span><br/> {item.year}</div>
      <div><span className="field-name">Country:</span><br/> {item.country}</div>
      <div><span className="field-name">Description:</span><br/> {item.description}</div>
      <br/>
      <button onClick={() => this.onDelete()}>Delete</button>&nbsp;
      <button onClick={() => this.onEdit()}>Edit</button>
      </div>
    );
  }

  onEdit() {
    this.props.onEdit();
  }

  onDelete() {
    const item = this.props.item;
    if(window.confirm("Are you sure to delete item: " + item.name + " ?")) {
      this.props.onDelete(item.link);
    }
  }

}

export default ItemDetails;

Листинг 3.5. src / edit-item.js — редактирование существующих элементов:

import React, { Component } from 'react';
import './App.css';
import Validator from './shared/validator';

class EditItem extends Component {

  constructor(props) {
    super(props);
    this.validator = new Validator();
    this.onCancel = this.onCancel.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
    const itemToEdit = props.item;
    this.state = {
      name: itemToEdit.name,
      summary: itemToEdit.summary,
      year: itemToEdit.year,
      country: itemToEdit.country,
      description: itemToEdit.description,
      link: itemToEdit.link
    };
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  onCancel() {
    this.props.onCancel();
  }

  onSubmit() {
    if (this.validator.validateInputs(this.state)) {
      this.props.onSubmit(this.state);
    }
  }

  render() {
    return (
      <div className="input-panel">
      <span className="form-caption">Edit item:</span>&nbsp;<span>{this.state.name}</span>
      <div>
        <label className="field-name">Name:<br/>
          <input value={this.state.name} name="name" maxLength="40" required onChange={this.handleInputChange} placeholder="item name" />
        </label>
      </div>
      <div>
        <label className="field-name">Summary:<br/>
          <input value={this.state.summary} name="summary" maxLength="40" required onChange={this.handleInputChange} placeholder="summary" />
        </label>
      </div>
      <div>
        <label className="field-name">Year:<br/>
          <input value={this.state.year} name="year" maxLength="4" pattern="[0-9]{1,4}" onChange={this.handleInputChange} placeholder="year" />
        </label>
      </div>
      <div>
        <label className="field-name">Country:<br/>
          <input value={this.state.country} name="country" maxLength="2" pattern="[a-z|A-Z]{2}" onChange={this.handleInputChange} placeholder="country" />
        </label>
      </div>
      <div>
        <label className="field-name">Description:<br/>
          <textarea value={this.state.description} name="description" onChange={this.handleInputChange} placeholder="description" />
        </label>
      </div>
      <br/>
      <button onClick={() => this.onCancel()}>Cancel</button>&nbsp;
      <button onClick={() => this.onSubmit()}>Update</button>
      </div>
    );
  }
}

export default EditItem;

Здесь мы используем подход с подъемом состояния. Вместо того чтобы поддерживать состояние в каждом дочернем компоненте и синхронизировать состояния и, следовательно, появление связанных компонентов, мы поднимаем общее состояние до их ближайшего общего предка. Итак, мы поддерживаем состояние в родительском  app компоненте с помощью функций обратного вызова, которые передаются дочерним компонентам через свойства. Затем мы вызываем функции обратного вызова внутри обработчиков событий в дочерних компонентах. В этих функциях мы изменяем состояние родительского компонента в соответствии с действиями пользователя, инициированными в дочерних компонентах. Основываясь на изменении состояния родительского компонента, React при необходимости повторно отображает дочерние компоненты. Например, посмотрите, как App.onEditItem() метод вызывается в ItemDetails.onEdit() обработчике событий, который срабатывает, когда пользователь нажимает кнопку «Изменить».

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

Примечание. Технология Redux обеспечивает еще более согласованный и эффективный способ управления состоянием модели компонента, особенно в крупных приложениях.

При условии, что у нас есть все сценарии, мы можем увидеть основное приложение по адресу  http: // localhost: 3000 :

Нажав на элемент в списке, мы можем увидеть детали элемента:

Если нам нужно отредактировать элемент, мы можем сделать подробный вид редактируемым с помощью кнопки «Редактировать»:

Также мы можем добавлять новые элементы с помощью кнопки New Item:

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

4. Реальное общение

Хотя React не предоставляет никакой встроенной поддержки для отправки запросов на сервер, мы можем использовать любую коммуникационную библиотеку в наших приложениях React. Давайте использовать Fetch API, который становится стандартным способом отправки HTTP-запросов и поддерживается в большинстве современных браузеров. Если у нас есть определенный интерфейс связи, мы можем легко заменить нашу реализацию фиктивного сервиса (см. Раздел 2) полнофункциональной версией, как показано ниже.

Листинг 4.1. src / shared / item-service, js — реальная функциональная версия ItemService:

import Configuration from './configuration';

class ItemService {

  constructor() {
    this.config = new Configuration();
  }

  async retrieveItems() {
    return fetch(this.config.ITEM_COLLECTION_URL)
      .then(response => {
        if (!response.ok) {
          this.handleResponseError(response);
        }
        return response.json();
      })
      .then(json => {
        console.log("Retrieved items:");
        console.log(json);
        const items = [];
        const itemArray = json._embedded.collectionItems;
        for(var i = 0; i < itemArray.length; i++) {
          itemArray[i]["link"] =  itemArray[i]._links.self.href;
          items.push(itemArray[i]);
        }
        return items;
      })
      .catch(error => {
        this.handleError(error);
      });
  }

  async getItem(itemLink) {
    console.log("ItemService.getItem():");
    console.log("Item: " + itemLink);
    return fetch(itemLink)
      .then(response => {
        if (!response.ok) {
            this.handleResponseError(response);
        }
        return response.json();
      })
      .then(item => {
          item["link"] = item._links.self.href;
          return item;
        }
      )
      .catch(error => {
        this.handleError(error);
      });
  }

  async createItem(newitem) {
    console.log("ItemService.createItem():");
    console.log(newitem);
    return fetch(this.config.ITEM_COLLECTION_URL, {
      method: "POST",
      mode: "cors",
      headers: {
            "Content-Type": "application/json"
        },
      body: JSON.stringify(newitem)
    })
      .then(response => {
       if (!response.ok) {
            this.handleResponseError(response);
        }
        return response.json();
      })
      .catch(error => {
        this.handleError(error);
      });
  }

  async deleteItem(itemlink) {
    console.log("ItemService.deleteItem():");
    console.log("item: " + itemlink);
    return fetch(itemlink, {
      method: "DELETE",
      mode: "cors"
    })
      .then(response => {
        if (!response.ok) {
            this.handleResponseError(response);
        }
      })
      .catch(error => {
        this.handleError(error);
      });
  }

  async updateItem(item) {
    console.log("ItemService.updateItem():");
    console.log(item);
    return fetch(item.link, {
      method: "PUT",
      mode: "cors",
      headers: {
            "Content-Type": "application/json"
          },
      body: JSON.stringify(item)
    })
      .then(response => {
        if (!response.ok) {
          this.handleResponseError(response);
        }
        return response.json();
      })
      .catch(error => {
        this.handleError(error);
      });
  }

  handleResponseError(response) {
      throw new Error("HTTP error, status = " + response.status);
  }

  handleError(error) {
      console.log(error.message);
  }

}
export default ItemService;

Здесь мы также следуем принципу единой ответственности и объединяем все параметры конфигурации в один объект  Configuration, который можно импортировать во все соответствующие компоненты.

Теперь мы разработали все основные модули и можем собрать все вместе и запустить наше приложение.

5. Запуск интерфейсного приложения

При условии, что наш бэкэнд работает на http: // localhost: 8080 , мы можем установить его URL в классе конфигурации.

Листинг 5.1. Класс конфигурации — одноточечная настройка приложения:

class Configuration {

  ITEM_COLLECTION_URL = "http://localhost:8080/collectionItems";

}
export default Configuration;

И запустите наше приложение:

.../project-root/consuming-rest>npm start

На этот раз мы видим основной экран приложения с реальными данными из бэкэнда:

Мы можем добавить новые элементы, как показано на следующем скриншоте:

Добавлен новый предмет:

Итак, мы разработали полнофункциональное веб-приложение, которое поддерживает основные операции управления коллекциями, то есть возможность добавлять, просматривать, обновлять и удалять элементы. Исходный код этой статьи доступен по адресу https://github.com/spylypets/consuming-rest . Используя компонентную модель React, мы можем создавать сложные пользовательские интерфейсы с вложенными многостраничными представлениями, обеспечивая богатый пользовательский опыт. Более подробную информацию можно найти на  официальном сайте React.js и сайтах по связанным технологиям, например:

  • Redux — государственная библиотека управления.

  • Formik — библиотека поддержки форм HTML.

  • Jest  — Тестирование приложений React.


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