Быстрый старт с Laravel 5.5 + Vue.js: простой CRUD

Vue.js становится все более популярным, его преимущества состоят в том, что с ним очень быстро начать разработку. Давайте же убедимся в этом на практике, создав простой проект.

Что мы хотим создать?

В нашем примере мы создадим CRUD (Create / Read / Update / Delete) приложение, для управления списком компаний.

Для начала мы создадим основной проект на Laravel, а затем добавим логику на Vue.js.

Этап 1: Типичный проект на Laravel

Шаг 1: Создадим новый проект Laravel с помощью composer:

composer create-project laravel/laravel="5.5.*" laravuecrud

Шаг 2: Выполните команду php artisan make:auth что бы создать базовые лайауты с Bootstrap и классы авторизации на сайте.

Шаг 3: Создайте новый шаблон куда мы будем выводит список компаний, я назвал его resources/views/admin/companies/index.blade.php и вот как он у нас должен выглядеть на данный момент:

@extends('layouts.app')
 
@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Companies</div>
 
                    <div class="panel-body">
                        Coming soon...
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Этап 2: Подготовка базы данный и API

Шаг 1. Нам понадобится модель и база данных. Давайте же их создадим:

php artisan make:model Company -m

Эта команда создаст файл app/Company.php в который необходимо добавить поля доступные для записи в БД:

class Company extends Model
{
    protected $fillable = ['name', 'address', 'website', 'email'];
}

Аналогично - поля в файле миграции, который был сгенерирован автоматически благодаря флагу -m. Приведём миграцию к виду:

public function up()
{
    Schema::create('companies', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name')->nullable();
        $table->string('address')->nullable();
        $table->string('website')->nullable();
        $table->string('email')->nullable();
        $table->timestamps();
    });
}
 

Теперь давайте создадим контроллер, который будет управлять операциями CRUD. Но это будет не просто команда make:controller - мы должны создать контроллек как API-интерфейс, типа такого:

Для этого нам нужно видоизменить привычную нам команду make и указать полный путь:

php artisan make:controller Api/V1/CompaniesController --resource

Теперь добавим типичные операци которые содержит любое CRUD приложение:

namespace App\Http\Controllers\Api\V1;
 
use App\Company;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
 
class CompaniesController extends Controller
{
    public function index()
    {
        return Company::all();
    }
 
    public function show($id)
    {
        return Company::findOrFail($id);
    }
 
    public function update(Request $request, $id)
    {
        $company = Company::findOrFail($id);
        $company->update($request->all());
 
        return $company;
    }
 
    public function store(Request $request)
    {
        $company = Company::create($request->all());
        return $company;
    }
 
    public function destroy($id)
    {
        $company = Company::findOrFail($id);
        $company->delete();
        return '';
    }
}

Ну и наконец нам осталось создать роунинг для нашего API в файле: routes/api.php

Route::group(['prefix' => '/v1', 'namespace' => 'Api\V1', 'as' => 'api.'], function () {
    Route::resource('companies', 'CompaniesController', ['except' => ['create', 'edit']]);
});

Как вы можете видеть, я добавляю префикс api. и исключая методы create и edit, потому что они не имеют смысла без визуальных форм для API.

Готово, теперь мы перейдём к самому интересному, созданию части фронтенда на VUE.JS

Этап 3: Стартуем с Vue.js

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


/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');

window.Vue = require('vue');

/**
 * Next, we will create a fresh Vue application instance and attach it to
 * the page. Then, you may begin adding components to this application
 * or customize the JavaScript scaffolding to fit your unique needs.
 */

Vue.component('example', require('./components/Example.vue'));

const app = new Vue({
    el: '#app'
});

Как вы видите здесь уже создан первый vue компонент который инициирует приложение Vue в дом элементе с ID #app.

Теперь нам понадобится дополнительная библиотека для Vue - Vue Router, давайте же её установим. (предполагается что на вашей машине установлен Node.Js и вы имеете хотя бы смутное представление как он работает)

Запускаем команду:

npm install && npm install vue-router

Теперь нам нужно скомпилировать бандл нашего фронтенд приложения в файл app.js который уже подключен в нашем шаблоне. Для этого запуститье команду:

npm run watch

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

Этап 4: компонент Vue роутер

Следующим шагом будет фактически использование Vue роутера и присваивание его различным представлениям в нашем CRUD приложении. 

Для начала давайте откроем ранее созданное представление resources/views/admin/companies/index.blade.php и добавим в него загрузку роутера:

...
<div class="panel-heading">Companies</div>
 
<div class="panel-body table-responsive">
    <router-view name="companiesIndex"></router-view>
    <router-view></router-view>
</div>
...
 

Обратите внимание на строки с псевдо тегами <router-view> это то место куда будет выводиться список наших компаний. Давайте-же создадим его!

Откройте файл resources/assets/js/app.js и добавьте туда вот этот компонент:

/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');

window.Vue = require('vue');
import VueRouter from 'vue-router';

window.Vue.use(VueRouter);

import CompaniesIndex from './components/companies/CompaniesIndex.vue';
import CompaniesCreate from './components/companies/CompaniesCreate.vue';
import CompaniesEdit from './components/companies/CompaniesEdit.vue';

const routes = [
    {
        path: '/',
        components: {
            companiesIndex: CompaniesIndex
        }
    },
    {path: '/admin/companies/create', component: CompaniesCreate, name: 'createCompany'},
    {path: '/admin/companies/edit/:id', component: CompaniesEdit, name: 'editCompany'},
]

const router = new VueRouter({ routes })

const app = new Vue({ router }).$mount('#app')

Вы можете изучить документацию Vue на предмет того, как работает маршрутизатор в целом, но то, что мы тут делаем, это в основном, связано с каждым маршрутом, который нам нужен, для компонентов Vue, такими как CompaniesIndex, CompaniesCreate и CompaniesEdit.

Давайте же создадим выше описанные компоненты:

Создадим файл resources/assets/js/components/companies/CompaniesIndex.vue и наполним его вот таким содержимым (Далее мы отдельно разберём осноаные части):

<template>
    <div>
        <div class="form-group">
            <router-link :to="{name: 'createCompany'}" class="btn btn-success">Create new company</router-link>
        </div>
 
        <div class="panel panel-default">
            <div class="panel-heading">Companies list</div>
            <div class="panel-body">
                <table class="table table-bordered table-striped">
                    <thead>
                    <tr>
                        <th>Name</th>
                        <th>Address</th>
                        <th>Website</th>
                        <th>Email</th>
                        <th width="100">&nbsp;</th>
                    </tr>
                    </thead>
                    <tbody>
                    <tr v-for="company, index in companies">
                        <td>{{ company.name }}</td>
                        <td>{{ company.address }}</td>
                        <td>{{ company.website }}</td>
                        <td>{{ company.email }}</td>
                        <td>
                            <router-link :to="{name: 'editCompany', params: {id: company.id}}" class="btn btn-xs btn-default">
                                Edit
                            </router-link>
                            <a href="#"
                               class="btn btn-xs btn-danger"
                               v-on:click="deleteEntry(company.id, index)">
                                Delete
                            </a>
                        </td>
                    </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</template>
 
<script>
    export default {
        data: function () {
            return {
                companies: []
            }
        },
        mounted() {
            var app = this;
            axios.get('/api/v1/companies')
                .then(function (resp) {
                    app.companies = resp.data;
                })
                .catch(function (resp) {
                    console.log(resp);
                    alert("Could not load companies");
                });
        },
        methods: {
            deleteEntry(id, index) {
                if (confirm("Do you really want to delete it?")) {
                    var app = this;
                    axios.delete('/api/v1/companies/' + id)
                        .then(function (resp) {
                            app.companies.splice(index, 1);
                        })
                        .catch(function (resp) {
                            alert("Could not delete company");
                        });
                }
            }
        }
    }
</script>

Выглядит знакомо, не так ли? Это та самая таблица, которая появится в index.blade.php только с некоторым вкусом Vue.js.

  • Строка <router-link :to=”{name: ‘createCompany’}”> создает ссылку на компонент, который будет загружаться без перезагрузки страницы;
  • Строка таблицы <tr v-for=”company, index in companies”> загрузит данные и отрисует каждое поле;
  • Данные для таблицы взяты из API через axiox (так-же предустановлен в Laravel): axios.get(‘/api/v1/companies’) -> app.companies = resp.data;
  • Также есть кнопка удаления, которая также вызывает соотвествующий метод в API axios.delete(‘/api/v1/companies/’ + id) который после отработки перезагружает таблицу, без перезагрузки страницы разумеется.

В принципе, это всё, что необходимо для отрисовки списка компаний. Компонент Vue отрисует пустую таблицу и затем заполнит её данными из API.

Этап 5: Завершение, компоненты Create и Edit

Теперь вы знаете, как устроены  Vue компоненты и как их добавлять к URL-адресам через VueRouter. Закончим наш демо-проект с помощью форм создания и редактирования компаний, которые будут очень похожи.

Создаём файл компонента resources/assets/js/components/companies/CompaniesCreate.vue:

<template>
    <div>
        <div class="form-group">
            <router-link to="/" class="btn btn-default">Back</router-link>
        </div>

        <div class="panel panel-default">
            <div class="panel-heading">Create new company</div>
            <div class="panel-body">
                <form v-on:submit="saveForm()">
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <label class="control-label">Company name</label>
                            <input type="text" v-model="company.name" class="form-control">
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <label class="control-label">Company address</label>
                            <input type="text" v-model="company.address" class="form-control">
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <label class="control-label">Company website</label>
                            <input type="text" v-model="company.website" class="form-control">
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <label class="control-label">Company email</label>
                            <input type="text" v-model="company.email" class="form-control">
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <button class="btn btn-success">Create</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        data: function () {
            return {
                company: {
                    name: '',
                    address: '',
                    website: '',
                    email: '',
                }
            }
        },
        methods: {
            saveForm() {
                event.preventDefault();
                var app = this;
                var newCompany = app.company;
                axios.post('/api/v1/companies', newCompany)
                    .then(function (resp) {
                        app.$router.push({path: '/'});
                    })
                    .catch(function (resp) {
                        console.log(resp);
                        alert("Could not create your company");
                    });
            }
        }
    }
</script>

Что у нас тут происходит?

  • Тоже самое - тег <template> для отрисовки шаблона содержимого и <script> для JS логики
  • Присвоение полей ввода полям модели:  input type=”text” v-model=”company.name”
  • Форма с событием submit  v-on:submit=”saveForm()” и метод, определяемый при вызове API: axios.post(‘/api/v1/companies’, newCompany)

И снова, после отправки формы Vue.js перезагрузит таблицу с содержимым, но опять же - без перезагрузки всей страницы.

Ну и наконец создадим компонент resources/assets/js/components/companies/CompaniesEdit.vue который практически аналогичен предыдущему:

<template>
    <div>
        <div class="form-group">
            <router-link to="/" class="btn btn-default">Back</router-link>
        </div>

        <div class="panel panel-default">
            <div class="panel-heading">Create new company</div>
            <div class="panel-body">
                <form v-on:submit="saveForm()">
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <label class="control-label">Company name</label>
                            <input type="text" v-model="company.name" class="form-control">
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <label class="control-label">Company address</label>
                            <input type="text" v-model="company.address" class="form-control">
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <label class="control-label">Company website</label>
                            <input type="text" v-model="company.website" class="form-control">
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <label class="control-label">Company email</label>
                            <input type="text" v-model="company.email" class="form-control">
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-xs-12 form-group">
                            <button class="btn btn-success">Create</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</template>
<script>
    export default {
        mounted() {
            let app = this;
            let id = app.$route.params.id;
            app.companyId = id;
            axios.get('/api/v1/companies/' + id)
                .then(function (resp) {
                    app.company = resp.data;
                })
                .catch(function () {
                    alert("Could not load your company")
                });
        },
        data: function () {
            return {
                companyId: null,
                company: {
                    name: '',
                    address: '',
                    website: '',
                    email: '',
                }
            }
        },
        methods: {
            saveForm() {
                event.preventDefault();
                var app = this;
                var newCompany = app.company;
                axios.patch('/api/v1/companies/' + app.companyId, newCompany)
                    .then(function (resp) {
                        app.$router.replace('/');
                    })
                    .catch(function (resp) {
                        console.log(resp);
                        alert("Could not create your company");
                    });
            }
        }
    }
</script>

Вот собственно и всё, наше приложение готово!