Статьи

Ultimate Vue.js и Laravel CRUD Tutorial

CRUD (создание, чтение, обновление и удаление) — это основные операции хранения данных и одна из первых вещей, которые вы изучаете как разработчик Laravel.

Но что происходит, когда вы добавляете одностраничное приложение Vue.js в качестве внешнего интерфейса к этому стеку? Внезапно вам приходится сталкиваться с асинхронным CRUD, поскольку операции теперь выполняются без обновления страницы. Это потребует особого внимания к обеспечению того, чтобы состояние данных было согласованным как во внешнем интерфейсе, так и во внутреннем.

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

Вы можете проверить готовый продукт в этом репозитории GitHub .

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

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

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

CRUD в бэкэнде Laravel

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

В итоге мы будем:

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

База данных

Во-первых, миграция. У наших Cruds есть два свойства: имя и цвет, которые мы храним как текст.

2018_02_02_081739_create_cruds_table.php

<?php

...

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мы их исключим.

маршруты / api.php

<?php

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

Имея это в виду, вот различные конечные точки, которые мы теперь будем иметь в нашем API:

контроллер

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

приложение / HTTP / Контроллеры / CrudsController.php

*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
{
  // Methods
}

Давайте сделаем краткий обзор каждого метода:

создать . Мы рандомизируем название и цвет нового Crud, используя Fakerпакет, включенный в Laravel. Мы отправляем новые данные Crud обратно в формате JSON.

<?php

...

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);
}

индекс . Мы возвращаем полный набор Cruds с indexметодом. В более серьезных приложениях мы использовали бы нумерацию страниц, но давайте пока будем проще.

<?php

...

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

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

<?php

...

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

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

уничтожить . Вот как мы удаляем наши Cruds.

<?php

...

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

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

Vue.js App

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

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

  • Показанное изображение зависит от цвета Crud ( red.png или green.png ).
  • Имеет кнопку удаления, которая вызывает метод delпри нажатии, который генерирует событие deleteс идентификатором Crud.
  • Имеет выбор HTML (для выбора цвета), который запускает метод updateпри изменении, который генерирует событие updateс идентификатором Crud и новым выбранным цветом.

ресурсы / активы / JS / компоненты / CrudComponent.vue

<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.js . Именно здесь происходит вся интересная логика, поэтому мы пройдем этот шаг за шагом.

Начнем с шаблона. Это имеет следующие рабочие места:

  • Покажите наши Cruds с crud-componentкомпонентом, обсужденным выше.
  • Цикл по массиву объектов Crud (в массиве cruds), с каждым сопоставлением экземпляру crud-component. Проходят все свойства Crud через компоненту в качестве реквизита и настроить слушатель для updateи deleteсобытий , поступающих из компонента.
  • У нас также есть кнопка Добавить , которая будет создавать новые Cruds, вызывая метод createпо щелчку.

ресурсы / активы / JS / компоненты / App.vue

<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.js . Давайте поговорим об этом тоже:

  • Мы начнем с функции, Crudкоторая создает новые объекты, используемые для представления наших Cruds. У каждого есть идентификатор, цвет и имя.
  • Импортируем соседние CrudComponent
  • Определение компонента содержит массив crudsв качестве свойства данных. Я также обозначил методы для каждой операции CRUD, которые будут описаны в следующем разделе.

ресурсы / активы / JS / компоненты / App.vue

<template>...</template>
<script>
  function Crud({ id, color, name}) {
    this.id = id;
    this.color = color;
    this.name = name;
  }

  import CrudComponent from './CrudComponent.vue';

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

Запуск CRUD с внешнего интерфейса с AJAX

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

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

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

Читать

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

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

Axios методы , такие как get, postи т.д. вернуть обещание. Мы связываем thenметод с обратным вызовом для доступа к ответу. Разрешенный объект может быть деструктурирован, чтобы обеспечить удобный доступ к dataсвойству в обратном вызове, который является телом ответа AJAX.

ресурсы / активы / JS / компоненты / App.vue

...

methods() {
  read() {
    window.axios.get('/api/cruds').then(({ data }) => {
      // 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массиву свойству данных , т.е. this.cruds.push(...).

ресурсы / активы / JS / компоненты / App.vue

...

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

Note: We need to trigger the read method programmatically when the app loads. We do this from the created hook, which works, but is not very efficient. It’d be far better to get rid of the read method altogether and just include the initial state of the app inlined into the document head when the first loads. I discuss this design pattern in depth in the article Avoid This Common Anti-Pattern In Full-Stack Vue/Laravel Apps if you want to implement it.

With that done, we can now see the Cruds displayed in our app when we load it:

Update (and Syncing State)

The update action requires us to send form data, i.e. color, so the controller knows what to update. The ID of the Crud is given in the endpoint.

This is a good time to discuss an issue I mentioned at the beginning of the article: with full-stack apps, you must ensure the state of the data is consistent in both the front and backends.

In the case of the update method, we could update the Crud object in the front-end app instantly before the AJAX call is made since we already know the new state.

However, we don’t perform this update until the AJAX call completes. Why? The reason is that the action might fail for some reason: the internet connection might drop, the updated value may be rejected by the database, or some other reason.

If we wait until the server responds before updating the front-end state, we can be sure the action was successful and the front and backend data is synchronized.

resources/assets/js/components/App.vue

methods: {
  read() {
    ...
  },
  update(id, color) {
    window.axios.put(`/api/cruds/${id}`, { color }).then(() => {
      // Once AJAX resolves we can update the Crud with the new color
      this.cruds.find(crud => crud.id === id).color = color;
    });
  },
  ...
}

You might argue its bad UX to wait for the AJAX to resolve before showing the changed data when you don’t have to, but I think it’s much worse UX to mislead the user into thinking a change is done, when in fact, we aren’t sure if it is done or not.

Create and Delete

Now that you understand the key points of the architecture, you will be able to understand these last two operations without my commentary:

resources/assets/js/components/App.vue

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

Loading Indicator and Disabling Interaction

As you know, our CRUD operations are asynchronous, and so there’s a small delay while we wait for the AJAX call to reach the server, for the server to respond, and to receive the response.

To improve UX, it’d be good to have some kind of visual loading indicator and to disable any interactivity while we wait for the current action to resolve. This lets the user know what’s going on, plus, it gives them certainty of the state of the data.

There are some good plugins for Vue.js loading state, but I’m just going to make something quick and dirty here: while AJAX is underway, I’ll overlay a full screen, semi-transparent div over the top of the app. This will kill the two above-mentioned birds with a single stone.

resources/views/index.blade.php

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

To do this, we’ll toggle the value of a boolean mute from false to true whenever AJAX is underway, and use this value to show/hide the div.

resources/assets/js/components/App.vue

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

Here’s how we implement the toggling of mute in the update method. When the method is called, mute is set to true. When the promise resolves, AJAX is done so it’s safe for the user to interact with the app again, so we set mute back to false.

resources/assets/js/components/App.vue

update(id, color) {
  this.mute = true;
  window.axios.put(`/api/cruds/${id}`, { color }).then(() => {
    this.cruds.find(crud => crud.id === id).color = color;
    this.mute = false;
  });
},

You’ll need to implement the same thing in each of the CRUD methods, but I won’t show that here for brevity.

To make our loading indicator markup and CSS, we add the element<div id="mute"></div> directly above our mount element <div id="app"></div>.

As you can see from the inline style, when the class on is added to <div id="mute">, it will completely cover the app, adding a greyish tinge and preventing any click events from reaching the buttons and selects:

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>

The last piece of the puzzle is to toggle the on class by utilizing a watch on the value of mute, which calls this method each time mute changes:

export default {
  ...
  watch: {
    mute(val) {
      document.getElementById('mute').className = val ? "on" : "";
    }
  }
}

With that done, you now have a working full-stack Vue/Laravel CRUD app with a loading indicator. Here it is again in its full glory:

Don’t forget to grab the code in this GitHub repo and leave me a comment if you have any thoughts or questions!