1. Code
  2. JavaScript
  3. React

Создание приложения на React и Laravel: часть 2, React

Scroll to top
This post is part of a series called Build a React App with Laravel Backend.
Build a React App With a Laravel RESTful Back End: Part 1, Laravel 9 API

Russian (Pусский) translation by Marat Amerov (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

Это вторая и последняя часть руководства по созданию приложения React с бекендом на Laravel. В первой части серии мы создали RESTful API, используя Laravel для основы приложения листинга продуктов. В этом руководстве мы будем разрабатывать фронтенд с помощью React.

Мы также рассмотрим все доступные варианты преодоления разрыва между Laravel и React. Чтобы понять этото руководство, вам не нужно следить за каждой частью. Если вы здесь, чтобы увидеть, как React и Laravel собираются вместе, вы можете пропустить первую часть. Вы должны отправиться на GitHub, клонировать репо и взять краткое описание ниже, чтобы начать.

Краткое Содержание

В предыдущем руководстве мы разработали приложение на Laravel, которое отвечает на вызовы API. Мы создали роуты, контроллер и модель для простого приложения листинга продуктов. Поскольку задача контроллера вернуть ответ на HTTP-запросы, раздел представления был полностью пропущен.

Затем мы обсудили методы обработки и проверки исключений с использованием Laravel. К концу руководство у нас был API на Laravel. Теперь мы можем использовать этот API для создания приложений как для веба, так и для широкого спектра мобильных устройств.

В этом руководстве мы будем фокусироваться на фронтенд. Первая половина руководства посвящена настройке React в среде Laravel. Я также познакомлю вас с Laravel Mix (поддерживается Laravel 5.4 и новее), который является API для компиляции ассетов. Во второй половине руководства мы начнем создавать приложение React с нуля.

Настройка React в Laravel

Laravel Mix был представлен в Laravel 5.4, и в настоящее время это идеальный способ подключения React и Laravel. С Laravel 5.5 весь процесс стал намного проще. Я описал оба метода ниже.

Использование команды React Preset (Laravel 5.5)

У Laravel 5.5 есть новая функция, которая позволяет вам подстроить код для React компонентов с помощью команды preset react. В предыдущих версиях Laravel настройка React с Laravel была не такой простой. Если вы используете последнюю версию Laravel, выполните следующую команду, чтобы добавить в проект пресет React.

1
php artisan preset react

Laravel по умолчанию поставляется с предустановкой Vue, и вышеупомянутая команда заменяет все экземпляры Vue на React. Примечательно, что если вам не нужен пресет, вы можете удалить его полностью, используя команду php artisan preset none.

Если все будет хорошо, это должно появиться в вашем терминале.

1
React scaffolding installed successfully.
2
Please run "npm install && npm run dev" to compile your fresh scaffolding.

Laravel использует Laravel Mix, который является прослойкой для webpack. Webpack, как вы уже знаете, является сборщиком модулей. Он разрешает все зависимости модуля и генерирует необходимые статические ресурсы для JavaScript и CSS. React требует для работы сборщик, и webpack отлично вписывается в эту роль. Таким образом, Laravel Mix - это слой, который находится поверх webpack и упрощает использование webpack в Laravel.

Хорошее понимание того, как работает Laravel Mix, важно, если вам нужно настроить конфигурацию webpack позднее. Команда React preset не дает нам никакой информации о том, как все работает под капотом. Итак, давайте удалим предустановку React и повторим шаги вручную.

Ручной метод (Laravel 5.4)

Если вы используете Laravel 5.4, или вам просто интересно узнать, как настроен Laravel Mix, выполните следующие шаги:

Установите react, react-dom и babel-preset-react с использованием npm. Возможно, неплохо было бы установить yarn. Не секрет, что Laravel и React предпочитают yarn по сравнению с npm.

Перейдите в webpack.mix.js, расположенный внутри корневого каталога вашего Laravel проекта. Это файл конфигурации, в котором вы объявляете, как ваши ассеты должны быть скомпилированы. Замените строку mix.js('resources/assets/js/app.js', 'public/js'); на mix.react('resources/assets/js/app.js', 'public/js');. app.js является точкой входа для наших JavaScript файлов, а скомпилированные файлы будут находиться внутри public/js. Запустите npm install в терминале, чтобы установить все зависимости.

Затем перейдите к resources/assets/js. Там уже есть папка с компонентами и несколько других JavaScript файлов. Компоненты React войдут в каталог components. Удалите существующий файл Example.vue и создайте новый файл для примера компонента React.

resources/assets/js/component/Main.js

1
import React, { Component } from 'react';
2
import ReactDOM from 'react-dom';
3
4
/* An example React component */
5
class Main extends Component {
6
    render() {
7
        return (
8
            <div>
9
                <h3>All Products</h3>

10
            </div>

11
        );
12
    }
13
}
14
15
export default Main;
16
17
/* The if statement is required so as to Render the component on pages that have a div with an ID of "root";  

18
*/
19
20
if (document.getElementById('root')) {
21
    ReactDOM.render(<Main />, document.getElementById('root'));
22
}

Обновите app.js, чтобы удалить весь связанный с Vue код и импортировать компонент React.

resources/assets/js/app.js

1
require('./bootstrap');
2
3
/* Import the Main component */
4
import Main from './components/Main';

Теперь нам просто нужно сделать ассеты доступными для представлений. Файлы представлений находятся внутри каталога resources/views. Давайте добавим тэг <script>  в welcome.blade.php, который является страницей по умолчанию, отображаемой при переходе на localhost:8000/. Удалите содержимое файла представления и замените его следующим кодом:

resources/views/welcome.blade.php

1
<!doctype html>
2
<html lang="{{ app()->getLocale() }}">
3
    <head>
4
        <meta charset="utf-8">
5
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
        <meta name="viewport" content="width=device-width, initial-scale=1">
7
        <title>Laravel React application</title>
8
        <link href="{{mix('css/app.css')}}" rel="stylesheet" type="text/css">
9
    </head>
10
    <body>
11
    <h2 style="text-align: center"> Laravel and React application </h2>
12
        <div id="root"></div>
13
        <script src="{{mix('js/app.js')}}" ></script>
14
    </body>
15
</html>

Наконец, выполните npm run dev или yarn run dev для компиляции ассетов. Если вы перейдете на localhost:8000, вы должны увидеть:

Laravel and React running togetherLaravel and React running togetherLaravel and React running together
React внутри представлений Laravel.

В package.json есть скрипт, который автоматически компилирует ассеты при обнаружении любых изменений. Чтобы включить этот режим, запустите npm run watch.

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

Разработка React приложения

Если вы новичок в React, вы найдете остальную часть руководства несколько сложной. Я рекомендую взять курс React Crash Course for Beginners, чтобы лучше познакомиться с концепциями React. Давайте начнем!

React приложение построено вокруг компонентов. Компоненты являются наиболее важной структурой в React, и у нас есть каталог для компонентов.

Компоненты позволяют разделить пользовательский интерфейс на самостоятельные, переиспользуемые фрагменты и подумать о каждой части отдельно. Концептуально компоненты похожи на функции JavaScript. Они принимают произвольные входные данные (называемые "props") и возвращают элементы React, описывающие, что должно появиться на экране.
— Official React Docs

Для приложения, которое мы создаем, мы начнем с базового компонента, который отобразит все продукты, возвращаемые сервером. Назовем его Main. Компонент должен сначала позаботиться о следующем:

  • Извлеките все продукты из API (GET /api/products).
  • Хранение данных о продукте в его стейте.
  • Отображение данных продукта.

React не является полноценным фреймворком, следовательно он не имеет никаких AJAX функций сам по себе. Я буду использовать fetch(), который является стандартным JavaScript API для извлечения данных с сервера. Но есть тонны альтернатив для выполнения AJAX запросов на сервер.

resources/assets/js/component/Main.js

1
import React, { Component } from 'react';
2
import ReactDOM from 'react-dom';
3
4
/* Main Component */
5
class Main extends Component {
6
7
  constructor() {
8
  
9
    super();
10
    //Initialize the state in the constructor

11
    this.state = {
12
        products: [],
13
    }
14
  }
15
  /*componentDidMount() is a lifecycle method

16
   * that gets called after the component is rendered

17
   */
18
  componentDidMount() {
19
    /* fetch API in action */
20
    fetch('/api/products')
21
        .then(response => {
22
            return response.json();
23
        })
24
        .then(products => {
25
            //Fetched product is stored in the state

26
            this.setState({ products });
27
        });
28
  }
29
30
 renderProducts() {
31
    return this.state.products.map(product => {
32
        return (
33
            /* When using list you need to specify a key

34
             * attribute that is unique for each list item

35
            */
36
            <li key={product.id} >
37
                { product.title } 
38
            </li>      

39
        );
40
    })
41
  }
42
  
43
  render() {
44
   /* Some css code has been removed for brevity */
45
    return (
46
        <div>
47
              <ul>
48
                { this.renderProducts() }
49
              </ul> 

50
            </div> 

51
      
52
    );
53
  }
54
}

Здесь мы инициализируем стейт products пустым массивом в конструкторе. Как только компонент монтируется, мы используем fetch() для извлечения продуктов из /api/products и сохранения их в стейт. Метод render используется для описания UI компонента. Все продукты рендерятся в виде списка.

Screenshot of the React Application - List of all productsScreenshot of the React Application - List of all productsScreenshot of the React Application - List of all products

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

Отображение данных о продуктах

Вот список вещей, которым нам необходимо уделить внимание:

  • Отслеживания продукта, который был кликнут. Назовем его currentProduct с начальным значением null.
  • При клике на название продукта обновляется this.state.currentProduct.
  • Справа отображаются детали соответствующего продукта. Пока продукт не выбран, отображается сообщение "No product selected".

resources/assets/js/component/Main.js

1
import React, { Component } from 'react';
2
import ReactDOM from 'react-dom';
3
4
/* Main Component */
5
class Main extends Component {
6
7
  constructor() {
8
  
9
    super();
10
11
    /* currentProduct keeps track of the product currently

12
     * displayed */
13
    this.state = {
14
        products: [],
15
        currentProduct: null
16
    }
17
  }
18
 
19
  componentDidMount() {
20
    //code omitted for brevity

21
  }
22
23
 renderProducts() {
24
    return this.state.products.map(product => {
25
        return (
26
            //this.handleClick() method is invoked onClick.

27
            <li onClick={
28
                () =>this.handleClick(product)} key={product.id} >
29
                { product.title } 
30
            </li>      

31
        );
32
    })
33
  }
34
  
35
   handleClick(product) {
36
    //handleClick is used to set the state

37
    this.setState({currentProduct:product});
38
  
39
  }
40
  
41
  render() {
42
   /* Some css code has been removed for brevity */
43
    return (
44
        <div>
45
              <ul>
46
                { this.renderProducts() }
47
              </ul> 

48
            </div> 

49
      
50
    );
51
  }
52
}

Здесь мы добавили createProduct в state и инициализировали его значением null. Строка onClick={ () =>this.handleClick(product) } вызывает метод handleClick() при клике на элемент списка. Метод handleClick() обновляет стейт currentProduct.

Теперь, чтобы отобразить данные продукта, мы можем либо отобразить его внутри компонента Main, либо создать новый компонент. Как упоминалось ранее, разделение UI на более мелкие компоненты - это React way. Поэтому мы создадим новый компонент и назовите его Product.

Компонент Product вложен в компонент Main. Компонент Main передает свой стейт в качестве props. Компонент Product принимает этот props в качестве входных данных и отображает соответствующую информацию.

resources/assets/js/component/Main.js

1
render() {
2
  return (
3
    /* The extra divs are for the css styles */
4
        <div>
5
            <div>
6
             <h3> All products </h3>

7
              <ul>
8
                { this.renderProducts() }
9
              </ul> 

10
            </div> 

11
          
12
            <Product product={this.state.currentProduct} />

13
        </div>

14
    );
15
  }
16
}

resources/assets/js/component/Product.js

1
import React, { Component } from 'react';
2
3
/* Stateless component or pure component

4
 * { product } syntax is the object destructing

5
 */
6
const Product = ({product}) => {
7
   
8
  const divStyle = {
9
      /*code omitted for brevity */
10
  }
11
12
  //if the props product is null, return Product doesn't exist

13
  if(!product) {
14
    return(<div style={divStyle}>  Product Doesnt exist </div>);

15
  }
16
    
17
  //Else, display the product data

18
  return(  
19
    <div style={divStyle}> 
20
      <h2> {product.title} </h2>

21
      <p> {product.description} </p>

22
      <h3> Status {product.availability ? 'Available' : 'Out of stock'} </h3>

23
      <h3> Price : {product.price} </h3>

24
     
25
    </div>

26
  )
27
}
28
29
export default Product ;

Приложение должно выглядеть примерно так:

Screenshot of the React application with product details displayedScreenshot of the React application with product details displayedScreenshot of the React application with product details displayed

Добавление нового продукта

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

Вот что я считаю необходимым для разработки этого функционала:

  • Новый компонент со стейтом, который отображает пользовательский интерфейс для формы ввода. Стейт компонента хранит данные формы.
  • При отправке дочерний компонент передает состояние компоненту Main с помощью коллбека.
  • Компонент Main имеет метод, например handleNewProduct(), который обрабатывает логику для запуска POST запроса . После получения ответа компонент Main обновляет свой стейт (оба this.state.products и this.state.currentProduct)

Это звучит не очень сложно, не так ли? Давайте сделаем это шаг за шагом. Сначала создадим новый компонент. Я назову его AddProduct.

resources/assets/js/component/AddProduct.js

1
class AddProduct extends Component {
2
3
  constructor(props) {
4
    super(props);
5
       /* Initialize the state. */
6
       this.state = {
7
          newProduct: {
8
              title: '',
9
              description: '',
10
              price: 0,
11
              availability: 0
12
          }
13
        }
14
    
15
    //Boilerplate code for binding methods with `this`

16
    this.handleSubmit = this.handleSubmit.bind(this);
17
    this.handleInput = this.handleInput.bind(this);
18
  }
19
  
20
  /* This method dynamically accepts inputs and stores it in the state */
21
  handleInput(key, e) {
22
    
23
    /*Duplicating and updating the state */
24
    var state = Object.assign({}, this.state.newProduct); 
25
    state[key] = e.target.value;
26
    this.setState({newProduct: state });
27
  }
28
 /* This method is invoked when submit button is pressed */
29
  handleSubmit(e) {
30
    //preventDefault prevents page reload   

31
    e.preventDefault();
32
    /*A call back to the onAdd props. The current

33
     *state is passed as a param

34
     */
35
    this.props.onAdd(this.state.newProduct);
36
  }
37
38
  render() {
39
    const divStyle = {
40
        /*Code omitted for brevity */ }
41
    
42
    return(
43
      <div> 
44
        <h2> Add new product </h2>

45
        <div style={divStyle}> 
46
        /*when Submit button is pressed, the control is passed to 

47
         *handleSubmit method 

48
         */
49
        <form onSubmit={this.handleSubmit}>
50
          <label> Title: 
51
           { /*On every keystroke, the handeInput method is invoked */ }
52
            <input type="text" onChange={(e)=>this.handleInput('title',e)} />

53
          </label>

54
          
55
          <label> Description: 
56
            <input type="text" onChange={(e)=>this.handleInput('description',e)} />

57
          </label>

58
          
59
         { /* Input fields for Price and availability omitted for brevity */}
60
61
          <input type="submit" value="Submit" />
62
        </form>

63
      </div>

64
    </div>)

65
  }
66
}
67
68
export default AddProduct;
69
  

Компонент в основном отображает форму ввода, и все входные значения сохраняются в стейте (this.state.newProduct). Затем при отправке формы вызывается метод handleSubmit(). Но AddProduct должен передать информацию обратно родителю, и мы делаем это с помощью коллбека.

Компонент Main, который является родительским, передает ссылку на функцию в параметрах. Дочерний компонент AddProduct в нашем случае вызывает эту функцию для уведомления родителя об изменении стейта. Итак, строка this.props.onAdd(this.state.newProduct); является примером коллбека, который уведомляет родительский компонент нового продукта.

Теперь в компоненте Main мы объявим <AddProduct />  следующим образом:

1
<AddProduct onAdd={this.handleAddProduct} /> 

Обработчик события onAdd привязан к методу компонента handleAddProduct(). Этот метод содержит код для выполнения POST запроса на сервер. Если ответ указывает, что продукт был успешно создан, обновляется стейт products и currentProducts.

1
 handleAddProduct(product) {
2
     
3
    product.price = Number(product.price);
4
    /*Fetch API for post request */
5
    fetch( 'api/products/', {
6
        method:'post',
7
        /* headers are important*/
8
        headers: {
9
          'Accept': 'application/json',
10
          'Content-Type': 'application/json'
11
        },
12
        
13
        body: JSON.stringify(product)
14
    })
15
    .then(response => {
16
        return response.json();
17
    })
18
    .then( data => {
19
        //update the state of products and currentProduct

20
        this.setState((prevState)=> ({
21
            products: prevState.products.concat(data),
22
            currentProduct : data
23
        }))
24
    })
25
26
  }  

Не забудьте связать метод handleProduct с классом, используя this.handleAddProduct = this.handleAddProduct.bind(this); в конструкторе. И вот окончательная версия приложения:

Screenshot of the final version of the applicationScreenshot of the final version of the applicationScreenshot of the final version of the application

Что дальше?

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

Логика для удаления продукта

1
  handleDelete() {
2
     
3
    const currentProduct = this.state.currentProduct;
4
    fetch( 'api/products/' + this.state.currentProduct.id, 
5
        { method: 'delete' })
6
        .then(response => {
7
          /* Duplicate the array and filter out the item to be deleted */
8
          var array = this.state.products.filter(function(item) {
9
          return item !== currentProduct
10
        });
11
     
12
        this.setState({ products: array, currentProduct: null});
13
14
    });
15
  }

Логика обновления существующего продукта

1
handleUpdate(product) {
2
3
    const currentProduct = this.state.currentProduct;
4
    fetch( 'api/products/' + currentProduct.id, {
5
        method:'put',
6
        headers: {
7
          'Accept': 'application/json',
8
          'Content-Type': 'application/json'
9
        },
10
        body: JSON.stringify(product)
11
    })
12
    .then(response => {
13
        return response.json();
14
    })
15
    .then( data => {
16
        /* Updating the state */
17
        var array = this.state.products.filter(function(item) {
18
          return item !== currentProduct
19
      })
20
        this.setState((prevState)=> ({
21
            products: array.concat(product),
22
            currentProduct : product
23
        }))
24
    }) 
25
  }

Что вам нужно сделать, это погрузиться, закатать рукова и закончить приложение, используя приведенную выше логику. Я дам вам подсказку: кнопка удаления должна идеально входить в компонент Product, тогда как функция обновления должна иметь свой собственный компонент. Я призываю вас принять этот вызов и закончить недостающие компоненты.

Заключение

Мы прошли долгий путь от того места, где мы начали. Во-первых, мы создали REST API, используя фреймворк Laravel. Затем мы обсудили наши варианты объеденения Laravel и React. Наконец, мы построили UI к API с помощью React.

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

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

Вы проводили экспериментиры с Laravel и React раньше? Что вы думаете? Поделитесь мыслями с нами в комментариях.