SPA CRUD на Laravel и Vue

Как сделать SPA на Laravel и Vue

В этом уроке я расскажу вам, как сделать полноценный SPA (Single Page Application — Одностраничное приложение) на Vue 2 с бэкэндом на Laravel 6, включающий каждую из операций CRUD (Create, Read, Update и Delete).

Ключом к этой архитектуре является AJAX, поэтому, в качестве HTTP-клиента, будем использовать Axios. Я также покажу вам способы обхода некоторых подводных камней этой архитектуры.

Будет удобно, если в процессе урока вы будете смотреть мою демку с GitHub.

Full-Stack Vue/Laravel CRUD

CRUD (Create — Создание, Read — Чтение, Update — Обновление и Delete — Удаление) — это основные операции с данными и одна из первых вещей, которой вы научитесь при работе с Laravel. Vue.js 2 является частью пакета laravel/ui для Laravel 6. Vue — отличный вариант для создания динамического пользовательского интерфейса для ваших CRUD-операций.

Стандартный подход в комбинировании Vue и Laravel заключается в создании компонентов Vue, а затем их переносе в файлы Blade. Но CRUD требует обновления страницы для отображения изменений в пользовательском интерфейсе.

Одностраничное приложение

Прекрасный UX («юзер экспириенс» — пользовательский опыт взаимодействия с интерфейсом) можно достичь, создав одностраничник на Vue и Laravel. CRUD-операции смогут выполняться асинхронно без обновления страницы.

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

Демо приложение

Демка позволяет пользователю создавать новые «Cruds», так, поднатужившись, я решил, что будут называться инопланетные существа со странными именами и способностью менять цвет с красного на зеленый и наоборот.

Cruds отображаются на главной странице. Пользователь может как создавать новые Cruds, так и удалять их или менять им цвет.

SPA на Laravel и Vue

Настраиваем CRUD на бэкэнде Laravel

Начнем с бэкэнда, где выполняются CRUD-операции. Я буду краток, так как тема CRUD в Laravel и так широко освещена в других статьях и я предполагаю, что вы уже знакомы с основами.

В итоге, что нам нужно:

  • Настроить базу данных
  • Настроить маршруты RESTful API через контроллер ресурсов
  • Определить методы в контроллере для выполнения CRUD-операций

База данных

Создадим новую миграцию для создания таблицы наших Cruds. Cruds имеет два свойства: name и color, которые мы храним как текст.

class CreateCrudsTable extends Migration
{
  public function up()
  {
    Schema::create('cruds', function (Blueprint $table) {
      $table->increments('id');
      $table->text('name');
      $table->text('color');
      $table->timestamps();
    });
  }

  ...
}
...

API

Настроим маршруты RESTful API, которые понадобятся нашему одностраничнику. Метод resource фасада Route создаст все необходимые нам действия автоматически. Однако нам не нужно методы edit, show и store, поэтому мы их исключим.

// routes/api.php

Route::resource('/cruds', 'CrudsController', [
  'except' => ['edit', 'show', 'store']
]);

Вот маршруты, которые теперь у нас есть на API бэкенда:

SPA на Laravel и Vue

Контроллер

Реализуем логику для каждого из этих методов в контроллере:

// app/Http/Controllers/CrudsController.php

namespace App\Http\Controllers;

use App\Crud;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Faker\Generator;

class CrudsController extends Controller
{
  // Метода
}

create.
Мы рандомизируем название и цвет нового Crud, используя пакет Faker из Laravel. И отправляем свежесозданного Crud обратно в Vue в JSON-формате.

public function create(Generator $faker)
{
  $crud = new Crud();
  $crud->name = $faker->lexify('????????');
  $crud->color = $faker->boolean ? 'red' : 'green';
  $crud->save();

  return response($crud->jsonSerialize(), Response::HTTP_CREATED);
}

index.
С помощью этого метода index мы возвращаем полный набор Cruds и снова в формате JSON. В более серьезных приложениях мы бы использовали нумерацию страниц (pagination), но пока сделаем попроще.

public function index()
{
  return response(Crud::all()->jsonSerialize(), Response::HTTP_OK);
}

update.
Этот метод позволяет клиенту изменить цвет Crud.

public function update(Request $request, $id)
{
  $crud = Crud::findOrFail($id);
  $crud->color = $request->color;
  $crud->save();

  return response(null, Response::HTTP_OK);
}

destroy.
А так мы удаляем наши Cruds.

public function destroy($id)
{
  Crud::destroy($id);

  return response(null, Response::HTTP_OK);
}

Создание одностраничника на Vue.js

Настало время одностраничного приложения на Vue.js. Он заработает, либо с Laravel 5.x, либо с Laravel 6 с пакетом laravel/ui. Предполагаю, что вы знакомы с основами Vue.js, поэтому не буду объяснять элементарные вещи, типа компонентов и т.д.

Компонент CRUD

Начнем с создания в каталоге resources/assets/js/components компонента CrudComponent.vue для отображения наших Cruds.

SPA на Laravel и Vue

Этот компонент в основном будет использоваться для отображения и в нем не так много логики. Вот его аспекты, заслуживающие внимания:

  • Показанное изображение зависит от цвета Crud ( red.png или green.png )
  • Есть кнопка удаления, которая по клинку вызывает метод del, генерирующий событие delete с идентификатором Crud
  • Есть HTML селектор (для выбора цвета), который запускает метод update при изменениях, генерирующий событие update с идентификатором Crud и новым выбранным цветом
<template>
  <div class="crud">
    <div class="col-1">
      <img :src="image"/>
    </div>
    <div class="col-2">
      <h3>Name: {{ name | properCase }}</h3>
      <select @change="update">
        <option
          v-for="col in [ 'red', 'green' ]"
          :value="col"
          :key="col"
          :selected="col === color ? 'selected' : ''"
        >{{ col | properCase }}</option>
      </select>
      <button @click="del">Delete</button>
    </div>
  </div>
</template>
<script>
  export default {
    computed: {
      image() {
        return `/images/${this.color}.png`;
      }
    },
    methods: {
      update(val) {
        this.$emit('update', this.id, val.target.selectedOptions[0].value);
      },
      del() {
        this.$emit('delete', this.id);
      }
    },
    props: ['id', 'color', 'name'],
    filters: {
      properCase(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
      }
    }
  }
</script>
<style>...</style>


Компонент App

Vue-одностраничнику нужен корневой компонент, и это будет App.vue. Создайте этот файл в каталоге resources/assets/js. Убедитесь, что этот компонент монтируется основным экземпляром Vue, изменив содержимое app.js на:

window.Vue = require('vue');

import App from './App.vue';

const app = new Vue({
  el: '#app',
  components: {
    App
  },
  render: h => h(App)
});

Шаблон

Давайте создадим шаблон для App.vue. Он должен делать следующее:

  • Показывать наши Cruds с помощью компонента crud-component, о котором говорилось выше
  • Прокручивать в цикле объекты Crud (в массиве cruds), сопоставляя каждый с экземпляром компонента crud-component.
  • Мы передаем все свойства Crud соответствующему компоненту через входные параметры (props) и настраиваем слушателей для событий update и delete
  • Также есть кнопка «Add» (Добавить), которая будет создавать новые Cruds, вызывая по клику метод create
<template>
  <div id="app">
    <div class="heading">
      <h1>Cruds</h1>
    </div>
    <crud-component
      v-for="crud in cruds"
      v-bind="crud"
      :key="crud.id"
      @update="update"
      @delete="del"
    ></crud-component>
    <div>
      <button @click="create">Add</button>
    </div>
  </div>
</template>

Логика

Вот логика из секции script компонента App.vue:

  • Мы создаем фабричный метод Crud, который будет создавать новые объекты, используемых для воплощения наших Cruds. У каждого будет свой ID, цвет и имя.
  • Мы импортируем CrudComponent и используем его в этом компоненте
  • Мы предоставляем массив cruds в качестве данных
  • Я также обозначил методы для каждой операции CRUD, которые будут описаны в следующем разделе.
<template>...</template>
<script>
  function Crud({ id, color, name}) {
    this.id = id;
    this.color = color;
    this.name = name;
  }

  import CrudComponent from './components/CrudComponent.vue';

  export default {
    data() {
      return {
        cruds: []
      }
    },
    methods: {
      async create() {
        // To do
      },
      async read() {
        // To do
      },
      async update(id, color) {
        // To do
      },
      async del(id) {
        // To do
      }
    },
    components: {
      CrudComponent
    }
  }
</script>

Внедряем CRUD в Vue SPA через AJAX

Все CRUD-операции в приложении будут выполняться на бэкэнде, поскольку именно там находится база данных. Однако запуск операций будет производиться на Vue SPA.

Таким образом, здесь нам нужен HTTP-клиент (что-то, что может связывать наш фронт и бэкэнд). Axios — отличный HTTP-клиент, идущий в комплекте со стандартным фронтендом Laravel.

Давайте еще раз посмотрим на нашу таблицу ресурсов, так как каждый вызов AJAX должен быть нацелен на соответствующий маршрут API:

SPA на Laravel и Vue

Read (Чтение)

Начнем с метода read. Этот метод отвечает за получение наших Cruds из бэкэнда и указывает на index нашего контроллера в Laravel, используя конечную точку GET /api/cruds.

Мы можем сделать GET-вызов при помощью window.axios.get, так как библиотека Axios вызвана как свойство объекта windows в дефолтной настройке фронтенда Laravel.

Такие методы Axios, как get, post и т.д., возвращают промис (promise). Мы можем использовать async/await для аккуратной работы с объектом ответа. Затем деструктурируем ответ, чтобы получить свойство data являющееся телом ответа AJAX.

// resources/assets/js/components/App.vue
...

methods() {
  async read() {
    const { data } = window.axios.get('/api/cruds');
    // console.log(data)
  },
  ...
}

/*
Sample response:

[
  {
    "id": 0,
    "name": "ijjpfodc",
    "color": "green",
    "created_at": "2018-02-02 09:15:24",
    "updated_at": "2018-02-02 09:24:12"
  },
  {
    "id": 1,
    "name": "wjwxecrf",
    "color": "red",
    "created_at": "2018-02-03 09:26:31",
    "updated_at": "2018-02-03 09:26:31"
  }
]
*/

Как видите, Cruds возвращаются в виде JSON-массива. Axios автоматически парсит JSON и отдает нам JavaScript-объекты, что очень удобно. Давайте выполним итерацию и создадим новые Cruds с помощью нашей фабричного метода Crud, запушивая каждый новое существо в массив cruds.

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

// resources/assets/js/components/App.vue
...

methods() {
  async read() {
    const { data } = window.axios.get('/api/cruds');
    data.forEach(crud => this.cruds.push(new Crud(crud)));
  },
  ...
},
...
created() {
  this.read();
}

Когда мы это сделаем, то сможем увидеть наших Cruds в приложении после загрузки

SPA на Laravel и Vue

Примечание: загрузка данных из хука created работает, но не очень эффективно. Гораздо лучше было бы избавиться от read и просто включать начальное состояние приложения, инкапсулированного в заголовок документа при первой загрузке. Я не буду показывать сейчас вам, как это сделать, поскольку начнется бардак, но я подробно рассмотрел этот шаблон проектирования в статье «Избегайте использования этого анти-паттерна в приложениях на Vue/Laravel».

Update (Обновление) и сихронизация состояния

Действие update позволяет нам изменить цвет Crud. Мы отправим данные формы на эндпоинт API, чтобы она знала, какой цвет мы хотим использовать. Обратите внимание, что идентификатор Crud также указан в URL.

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

В случае метода update мы могли бы обновить объект Crud на фронтенеде непосредственно перед выполнением вызова AJAX, ведь мы уже знаем новое состояние.

Однако мы не сделаем этого, пока не завершится вызов AJAX. Почему? Потому, что действие может быть и не выполнено по какой-либо причине: может прерваться интернет-соединение, обновленное значение может быть отклонено базой данных или по какой-либо другой причине.

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

// resources/assets/js/components/App.vue

methods: {
  async read() {
    ...
  },
  async update(id, color) {
    await window.axios.put(`/api/cruds/${id}`, { color });
    // Как только AJAX завершится, то мы можем обновить цвет нашего Crud
    this.cruds.find(crud => crud.id === id).color = color;
  },
  ...
}

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

Create и Delete (Создание и Удаление)

Теперь, когда вы понимаете ключевые моменты архитектуры, последние две операции вы сможете понять без моих комментариев:

// resources/assets/js/components/App.vue

methods: {
  async read() {
    ...
  },
  async update(id, color) {
    ...
  },
  async create() {
    const { data } = window.axios.get('/api/cruds/create');
    this.cruds.push(new Crud(data));
  },
  async del(id) {
    await window.axios.delete(`/api/cruds/${id}`);
    let index = this.cruds.findIndex(crud => crud.id === id);
    this.cruds.splice(index, 1);
  }
}

Улучшения UX

Как вы знаете, наши CRUD-операции асинхронны, и поэтому существует небольшая задержка, пока AJAX-вызов достигнет сервера, сервер обработает и отправит ответ.

Для улучшения UX было бы хорошо иметь какой-то визуальный индикатор загрузки и отключать любой интерактив на время ожидания отработки текущего действия. Это поможет пользователю понять, что происходит и он будет уверенным в актуальном состоянии данных.

Есть несколько хороших плагинов для отображения состояния загрузки в Vue.js, но я сейчас быстренько сделаю на коленке простой вариант: пока отрабатывает AJAX, я наложу полноэкранный полупрозрачный div поверх приложения. Это убьет обоих зайцев одним выстрелом.

// resources/views/index.blade.php

<body>
    <div id="mute"></div>
    <div id="app"></div>
    <script src="js/app.js"></script>
</body>

Для этого мы, пока отрабатывает AJAX переключаем значение mute с false на true, и используем его, для отображения/скрытия div.

// resources/assets/js/components/App.vue

export default {
  data() {
    return {
      cruds: [],
      mute: false
    }
  },
  ...
}

Вот как реализуется переключение mute в методе update. Когда вызывается метод, то mute устанавливается в true. Когда промис получен, AJAX завершен и пользователь снова может безопасно работать с приложением — mute переключается обратно в false.

Вам нужно будет реализовать это в каждом из методов CRUD. Дабы не раздувать объемы я не буду показывать как это делается.

Для создания нашего индикатора загрузки мы добавляем элемент <div id="mute"></div> прямо над элементом <div id="app"></div>.

Как видно из встроенного стиля, когда класс on применяется к <div id="mute">, он полностью накрывает приложение, добавляя сероватый оттенок, и защищая его от кликов:

// resources/views/index.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">
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <title>Cruds</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      height: 100%;
      width: 100%;
      background-color: #d1d1d1
    }
    #mute {
      position: absolute;
    }
    #mute.on {
      opacity: 0.7;
      z-index: 1000;
      background: white;
      height: 100%;
      width: 100%;
    }
  </style>
</head>
<body>
<div id="mute"></div>
<div id="app"></div>
<script src="js/app.js"></script>
</body>
</html>

Теперь у вас есть рабочий фуллстек Vue/Laravel CRUD SPA с индикатором загрузки. Вот он снова во всей красе:

SPA на Laravel и Vue

Не забудьте взять код из этого GitHub репозитория!

Автор: Anthony Gore
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.