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



Это вторая и последняя часть руководства по созданию приложения 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.
php artisan preset react
Laravel по умолчанию поставляется с предустановкой Vue, и вышеупомянутая команда заменяет все экземпляры Vue на React. Примечательно, что если вам не нужен пресет, вы можете удалить его полностью, используя команду php artisan preset none
.
Если все будет хорошо, это должно появиться в вашем терминале.
React scaffolding installed successfully. 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
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; /* An example React component */ class Main extends Component { render() { return ( <div> <h3>All Products</h3> </div> ); } } export default Main; /* The if statement is required so as to Render the component on pages that have a div with an ID of "root"; */ if (document.getElementById('root')) { ReactDOM.render(<Main />, document.getElementById('root')); }
Обновите app.js, чтобы удалить весь связанный с Vue код и импортировать компонент React.
resources/assets/js/app.js
require('./bootstrap'); /* Import the Main component */ import Main from './components/Main';
Теперь нам просто нужно сделать ассеты доступными для представлений. Файлы представлений находятся внутри каталога resources/views. Давайте добавим тэг <script>
в welcome.blade.php, который является страницей по умолчанию, отображаемой при переходе на localhost:8000/
. Удалите содержимое файла представления и замените его следующим кодом:
resources/views/welcome.blade.php
<!doctype html> <html lang="{{ app()->getLocale() }}"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel React application</title> <link href="{{mix('css/app.css')}}" rel="stylesheet" type="text/css"> </head> <body> <h2 style="text-align: center"> Laravel and React application </h2> <div id="root"></div> <script src="{{mix('js/app.js')}}" ></script> </body> </html>
Наконец, выполните npm run dev
или yarn run dev
для компиляции ассетов. Если вы перейдете на localhost:8000, вы должны увидеть:



В 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
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; /* Main Component */ class Main extends Component { constructor() { super(); //Initialize the state in the constructor this.state = { products: [], } } /*componentDidMount() is a lifecycle method * that gets called after the component is rendered */ componentDidMount() { /* fetch API in action */ fetch('/api/products') .then(response => { return response.json(); }) .then(products => { //Fetched product is stored in the state this.setState({ products }); }); } renderProducts() { return this.state.products.map(product => { return ( /* When using list you need to specify a key * attribute that is unique for each list item */ <li key={product.id} > { product.title } </li> ); }) } render() { /* Some css code has been removed for brevity */ return ( <div> <ul> { this.renderProducts() } </ul> </div> ); } }
Здесь мы инициализируем стейт products
пустым массивом в конструкторе. Как только компонент монтируется, мы используем fetch()
для извлечения продуктов из /api/products и сохранения их в стейт. Метод render используется для описания UI компонента. Все продукты рендерятся в виде списка.



На странице просто скучный список названий продуктов. Более того, у нас пока нет интерактивных элементов. Давайте сделаем название продукта кликабельным, и при клике будет отображаться более подробная информация о продукте.
Отображение данных о продуктах
Вот список вещей, которым нам необходимо уделить внимание:
- Отслеживания продукта, который был кликнут. Назовем его
currentProduct
с начальным значениемnull
. - При клике на название продукта обновляется
this.state.currentProduct
. - Справа отображаются детали соответствующего продукта. Пока продукт не выбран, отображается сообщение "No product selected".
resources/assets/js/component/Main.js
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; /* Main Component */ class Main extends Component { constructor() { super(); /* currentProduct keeps track of the product currently * displayed */ this.state = { products: [], currentProduct: null } } componentDidMount() { //code omitted for brevity } renderProducts() { return this.state.products.map(product => { return ( //this.handleClick() method is invoked onClick. <li onClick={ () =>this.handleClick(product)} key={product.id} > { product.title } </li> ); }) } handleClick(product) { //handleClick is used to set the state this.setState({currentProduct:product}); } render() { /* Some css code has been removed for brevity */ return ( <div> <ul> { this.renderProducts() } </ul> </div> ); } }
Здесь мы добавили 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
render() { return ( /* The extra divs are for the css styles */ <div> <div> <h3> All products </h3> <ul> { this.renderProducts() } </ul> </div> <Product product={this.state.currentProduct} /> </div> ); } }
resources/assets/js/component/Product.js
import React, { Component } from 'react'; /* Stateless component or pure component * { product } syntax is the object destructing */ const Product = ({product}) => { const divStyle = { /*code omitted for brevity */ } //if the props product is null, return Product doesn't exist if(!product) { return(<div style={divStyle}> Product Doesnt exist </div>); } //Else, display the product data return( <div style={divStyle}> <h2> {product.title} </h2> <p> {product.description} </p> <h3> Status {product.availability ? 'Available' : 'Out of stock'} </h3> <h3> Price : {product.price} </h3> </div> ) } export default Product ;
Приложение должно выглядеть примерно так:



Добавление нового продукта
Мы успешно реализовали фронтенд, отвечающий за извлечение всех продуктов и их отображение. Затем нам нужна форма для добавления нового продукта в список продуктов. Процесс добавления продукта может показаться немного более сложным, чем просто получение данных из API.
Вот что я считаю необходимым для разработки этого функционала:
- Новый компонент со стейтом, который отображает пользовательский интерфейс для формы ввода. Стейт компонента хранит данные формы.
- При отправке дочерний компонент передает состояние компоненту Main с помощью коллбека.
- Компонент Main имеет метод, например
handleNewProduct()
, который обрабатывает логику для запуска POST запроса . После получения ответа компонент Main обновляет свой стейт (обаthis.state.products
иthis.state.currentProduct
)
Это звучит не очень сложно, не так ли? Давайте сделаем это шаг за шагом. Сначала создадим новый компонент. Я назову его AddProduct
.
resources/assets/js/component/AddProduct.js
class AddProduct extends Component { constructor(props) { super(props); /* Initialize the state. */ this.state = { newProduct: { title: '', description: '', price: 0, availability: 0 } } //Boilerplate code for binding methods with `this` this.handleSubmit = this.handleSubmit.bind(this); this.handleInput = this.handleInput.bind(this); } /* This method dynamically accepts inputs and stores it in the state */ handleInput(key, e) { /*Duplicating and updating the state */ var state = Object.assign({}, this.state.newProduct); state[key] = e.target.value; this.setState({newProduct: state }); } /* This method is invoked when submit button is pressed */ handleSubmit(e) { //preventDefault prevents page reload e.preventDefault(); /*A call back to the onAdd props. The current *state is passed as a param */ this.props.onAdd(this.state.newProduct); } render() { const divStyle = { /*Code omitted for brevity */ } return( <div> <h2> Add new product </h2> <div style={divStyle}> /*when Submit button is pressed, the control is passed to *handleSubmit method */ <form onSubmit={this.handleSubmit}> <label> Title: { /*On every keystroke, the handeInput method is invoked */ } <input type="text" onChange={(e)=>this.handleInput('title',e)} /> </label> <label> Description: <input type="text" onChange={(e)=>this.handleInput('description',e)} /> </label> { /* Input fields for Price and availability omitted for brevity */} <input type="submit" value="Submit" /> </form> </div> </div>) } } export default AddProduct;
Компонент в основном отображает форму ввода, и все входные значения сохраняются в стейте (this.state.newProduct
). Затем при отправке формы вызывается метод handleSubmit()
. Но AddProduct
должен передать информацию обратно родителю, и мы делаем это с помощью коллбека.
Компонент Main, который является родительским, передает ссылку на функцию в параметрах. Дочерний компонент AddProduct
в нашем случае вызывает эту функцию для уведомления родителя об изменении стейта. Итак, строка this.props.onAdd(this.state.newProduct);
является примером коллбека, который уведомляет родительский компонент нового продукта.
Теперь в компоненте Main мы объявим <AddProduct />
следующим образом:
<AddProduct onAdd={this.handleAddProduct} />
Обработчик события onAdd
привязан к методу компонента handleAddProduct()
. Этот метод содержит код для выполнения POST запроса на сервер. Если ответ указывает, что продукт был успешно создан, обновляется стейт products
и currentProducts
.
handleAddProduct(product) { product.price = Number(product.price); /*Fetch API for post request */ fetch( 'api/products/', { method:'post', /* headers are important*/ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(product) }) .then(response => { return response.json(); }) .then( data => { //update the state of products and currentProduct this.setState((prevState)=> ({ products: prevState.products.concat(data), currentProduct : data })) }) }
Не забудьте связать метод handleProduct
с классом, используя this.handleAddProduct = this.handleAddProduct.bind(this);
в конструкторе. И вот окончательная версия приложения:



Что дальше?
Приложение не является полноценным без функционала удаления и обновления. Но если вы внимательно следите за руководством до сих пор, вы должны иметь возможность реализовать это без особых проблем. Чтобы начать работу, я предоставил вам логику обработчика событий для сценария удаления и обновления.
Логика для удаления продукта
handleDelete() { const currentProduct = this.state.currentProduct; fetch( 'api/products/' + this.state.currentProduct.id, { method: 'delete' }) .then(response => { /* Duplicate the array and filter out the item to be deleted */ var array = this.state.products.filter(function(item) { return item !== currentProduct }); this.setState({ products: array, currentProduct: null}); }); }
Логика обновления существующего продукта
handleUpdate(product) { const currentProduct = this.state.currentProduct; fetch( 'api/products/' + currentProduct.id, { method:'put', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(product) }) .then(response => { return response.json(); }) .then( data => { /* Updating the state */ var array = this.state.products.filter(function(item) { return item !== currentProduct }) this.setState((prevState)=> ({ products: array.concat(product), currentProduct : product })) }) }
Что вам нужно сделать, это погрузиться, закатать рукова и закончить приложение, используя приведенную выше логику. Я дам вам подсказку: кнопка удаления должна идеально входить в компонент Product, тогда как функция обновления должна иметь свой собственный компонент. Я призываю вас принять этот вызов и закончить недостающие компоненты.
Заключение
Мы прошли долгий путь от того места, где мы начали. Во-первых, мы создали REST API, используя фреймворк Laravel. Затем мы обсудили наши варианты объеденения Laravel и React. Наконец, мы построили UI к API с помощью React.
Хотя мы в основном сосредоточились на создании одностраничного приложения с использованием React, вы можете создавать виджеты или компоненты, которые монтируются для определенных элементов в ваших представлениях. React очень гибкий, потому что это библиотека, и причем хорошая.
За последние пару лет React стал популярным. Кстати, у нас есть ряд элементов в маркете, которые доступны для покупки, обзора, реализации и так далее. Если вы ищете дополнительные ресурсы вокруг React, не стесняйтесь их исследовать.
Вы проводили экспериментиры с Laravel и React раньше? Что вы думаете? Поделитесь мыслями с нами в комментариях.