Форум программистов, компьютерный форум, киберфорум
Reangularity
Войти
Регистрация
Восстановить пароль

Создаем веб-приложение на Vue.js и Laravel

Запись от Reangularity размещена 23.04.2025 в 16:02
Показов 3047 Комментарии 0

Нажмите на изображение для увеличения
Название: 77a006df-3c29-4934-9cc4-7a46f0493e20.jpg
Просмотров: 73
Размер:	201.9 Кб
ID:	10633
Выбор правильного технологического стека определяет успех веб-проекта. Laravel и Vue.js формируют отличную комбинацию для создания современных приложений. Laravel — это PHP-фреймворк с элегантным синтаксисом и обширным набором инструментов для бэкенда. Vue.js — прогрессивный JavaScript-фреймворк, который упрощает создание динамичных пользовательских интерфейсов. Объединение этих технологий предоставляет разработчикам ряд существенных преимуществ. Laravel обеспечивает надежный серверный функционал с продуманной маршрутизацией, ORM-моделью Eloquent и множеством готовых решений. Vue.js привносит реактивность на фронтенд через компонентную архитектуру и виртуальный DOM, что значительно улучшает взаимодействие с пользователем.

Эта комбинация успешно решает различные задачи веб-разработки. Создание одностраничных приложений становится более доступным благодаря встроенной поддержке Vue.js в Laravel. Разработка RESTful API в Laravel с потреблением на стороне Vue.js позволяет строить приложения с четким разделением ответственности. Такой подход также упрощает разработку административных панелей и пользовательских порталов. При сравнении с другими популярными стеками, Laravel и Vue.js выделяются своей доступностью. React с Node.js предлагает универсальный JavaScript-подход, но требует более глубокого погружения. Angular с Node.js представляет структурированный, но сложный для освоения стек. Vue.js и Laravel обладают более плавной кривой обучения, что делает их доступными для команд разного уровня.

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

Laravel имеет встроенную поддержку Vue.js через Laravel Mix, что упрощает интеграцию и сборку фронтенд-ресурсов. Это позволяет разработчикам сосредоточиться на бизнес-логике вместо решения технических проблем. В этой статье мы охватим полный цикл разработки с использованием этого стека: настройку окружения, проектирование архитектуры, практическую реализацию и применение продвинутых техник, включая управление состоянием с Vuex и работу с очередями через Laravel Horizon.

Настройка окружения



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

Установка Laravel



Laravel требует PHP версии 7.4 или выше и Composer для управления зависимостями. Сначала убедитесь, что на вашей системе установлены необходимые компоненты. Для проверки версии PHP введите в терминале:

Bash
1
php -v
Если PHP не установлен или его версия устарела, скачайте актуальную версию с официального сайта. После установки PHP необходимо установить Composer – менеджер зависимостей для PHP. На Windows можно использовать установщик с официального сайта, а на macOS или Linux выполнить команду:

Bash
1
2
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
Теперь можно создать новый проект Laravel с помощью Composer:

Bash
1
composer create-project laravel/laravel my-project
Эта команда создаст новый каталог с именем "my-project", содержащий свежую копию фреймворка Laravel со всеми необходимыми зависимостями.

Установка Node.js и NPM



Поскольку Vue.js – это JavaScript-фреймворк, для работы с ним потребуется Node.js и менеджер пакетов NPM. Скачайте и установите актуальную версию Node.js с официального сайта. При установке Node.js автоматически устанавливается и NPM.
Чтобы проверить корректность установки, выполните команды:

Bash
1
2
node -v
npm -v
После установки Node.js перейдите в корневой каталог вашего Laravel-проекта и установите необходимые JavaScript-зависимости:

Bash
1
2
cd my-project
npm install

Интеграция Vue.js в Laravel-проект



Laravel имеет встроенную поддержку Vue.js через пакет Laravel Mix, который представляет собой обёртку над webpack. Сначала нужно установить Vue.js через NPM:

Bash
1
npm install vue@next
Затем откройте файл resources/js/app.js и добавьте код инициализации Vue:

JavaScript
1
2
3
4
import { createApp } from 'vue'
import App from './components/App.vue'
 
createApp(App).mount('#app')
Создайте компонент App.vue в директории resources/js/components:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
<template>
  <div>
    <h1>Мое первое приложение на Vue.js и Laravel</h1>
  </div>
</template>
 
<script>
export default {
  name: 'App'
}
</script>
Теперь нужно настроить Laravel для работы с Vue-компонентами. Установите пакет Laravel UI:

Bash
1
2
composer require laravel/ui
php artisan ui vue

Конфигурация webpack.mix.js



Laravel Mix – это утилита, которая значительно упрощает работу с webpack. Для настройки сборки фронтенд-ресурсов откройте файл webpack.mix.js в корневой директории проекта:

JavaScript
1
2
3
4
5
const mix = require('laravel-mix');
 
mix.js('resources/js/app.js', 'public/js')
   .vue()
   .sass('resources/sass/app.scss', 'public/css');
Эта конфигурация указывает Laravel Mix собирать JavaScript из файла resources/js/app.js в public/js/app.js, включая обработку Vue-компонентов, а также компилировать SASS-стили.

Для сборки ресурсов выполните:

Bash
1
npm run dev
Если вы хотите, чтобы ресурсы автоматически пересобирались при изменении файлов, используйте:

Bash
1
npm run watch

Подготовка базы данных и настройка .env файла



Для работы с базой данных скопируйте файл .env.example в новый файл .env:

Bash
1
cp .env.example .env
Откройте .env файл и настройте параметры подключения к базе данных:

Bash
1
2
3
4
5
6
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_vue_app
DB_USERNAME=root
DB_PASSWORD=your_password
Создайте новую базу данных с именем, указанным в настройках. После этого выполните миграции для создания структуры базы данных:

Bash
1
php artisan migrate
Для генерации ключа приложения выполните:

Bash
1
php artisan key:generate

Laravel Service Providers для Vue.js



Service Providers в Laravel выступают центральным местом конфигурации приложения. Для организации удобного взаимодействия между Laravel и Vue.js можно создать собственный сервис-провайдер:

Bash
1
php artisan make:provider VueServiceProvider
Откройте созданный файл app/Providers/VueServiceProvider.php и настройте его:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
 
namespace App\Providers;
 
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View;
 
class VueServiceProvider extends ServiceProvider
{
    public function register()
    {
        //
    }
 
    public function boot()
    {
        // Передача данных из Laravel в Vue.js
        View::composer('*', function ($view) {
            $view->with('authUser', auth()->user());
        });
    }
}
Затем зарегистрируйте этот сервис-провайдер в файле config/app.php:

PHP
1
2
3
4
'providers' => [
    // Другие провайдеры
    App\Providers\VueServiceProvider::class,
],
Таким образом, вы настроили базовое окружение для разработки приложения с использованием Laravel и Vue.js. В следующих разделах мы углубимся в архитектурные решения и практическое применение этих технологий.

Где завкладка “Vue” в консоле моего Vue.js приложения ?
Всем привет Я установил Vue.js devtools на Google Chrome Version 85.0(kubuntu 18) Но я не вижу...

Vue.js не видит пути из vue.config.js
Здравствуйте. Пытаюсь сделать многостраничное приложение. Следую инструкции для Vue.js 3, сделал...

Vue.draggable + vue-lazyload
Есть лист на 800+ элементов, и количество элементов будет только увеличиваться. Реализовано это всё...

vue 3. Как использовать метод из миксина с параметрами в компоненте vue по нажатию на кнопку?
Как вызвать метод query в компонент при клике на кнопку Subscribe axiosMixin.js const...


Расширенные настройки Laravel Mix



Laravel Mix не только упрощает сборку ресурсов, но и предлагает множество дополнительных возможностей. Для более гибкой конфигурации вашего проекта, файл webpack.mix.js можно расширить:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
const mix = require('laravel-mix');
 
mix.js('resources/js/app.js', 'public/js')
   .vue({ version: 3 })
   .sass('resources/sass/app.scss', 'public/css')
   .version()
   .sourceMaps()
   .browserSync({
     proxy: 'http://localhost:8000',
     open: false
   });
В этом примере мы добавили формирование версий файлов для обхода кэширования браузера, создание source maps для отладки и настроили BrowserSync для автоматического обновления страницы при изменении файлов.
Для запуска проекта с BrowserSync выполните:

Bash
1
npm run watch

Настройка маршрутизации для SPA приложений



Создание одностраничного приложения (SPA) требует особого подхода к маршрутизации. Laravel должен перенаправлять все запросы на главный шаблон, а Vue.js будет обрабатывать клиентскую маршрутизацию. Откройте файл routes/web.php и добавьте следующий маршрут:

PHP
1
2
3
Route::get('/{any}', function () {
    return view('app');
})->where('any', '.*');
Создайте файл шаблона resources/views/app.blade.php:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Laravel + Vue.js</title>
    <link href="{{ mix('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app"></div>
    <script src="{{ mix('js/app.js') }}"></script>
</body>
</html>

Настройка Vue Router



Для создания полноценного SPA необходимо настроить клиентскую маршрутизацию с помощью Vue Router:

Bash
1
npm install vue-router@4
Создайте файл resources/js/router/index.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../components/Home.vue'
import About from '../components/About.vue'
 
const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    component: About
  }
]
 
const router = createRouter({
  history: createWebHistory(),
  routes
})
 
export default router
Создайте компоненты для маршрутов в директории resources/js/components/. Например, Home.vue:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
<template>
  <div class="home">
    <h1>Домашняя страница</h1>
    <p>Добро пожаловать в наше приложение!</p>
  </div>
</template>
 
<script>
export default {
  name: 'Home'
}
</script>
Обновите ваш главный файл resources/js/app.js:

JavaScript
1
2
3
4
5
6
7
import { createApp } from 'vue'
import App from './components/App.vue'
import router from './router'
 
createApp(App)
  .use(router)
  .mount('#app')

Настройка взаимодействия с API



Для взаимодействия с бэкендом Vue.js обычно использует библиотеку Axios. Установите ее через NPM:

Bash
1
npm install axios
Создайте конфигурацию для Axios в файле resources/js/api/index.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
import axios from 'axios'
 
const api = axios.create({
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
  }
})
 
export default api
Не забудьте добавить мета-тег для CSRF-токена в ваш шаблон app.blade.php:

PHP
1
<meta name="csrf-token" content="{{ csrf_token() }}">

Настройка ESLint и Prettier



Для поддержания качества кода рекомендуется использовать ESLint и Prettier:

Bash
1
npm install --save-dev eslint eslint-plugin-vue prettier
Создайте файл конфигурации .eslintrc.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  extends: [
    'plugin:vue/vue3-recommended',
    'eslint:recommended'
  ],
  rules: {
    'vue/multi-word-component-names': 'off',
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  }
}
Добавьте скрипты в файл package.json:

JSON
1
2
3
4
"scripts": {
  "lint": "eslint --ext .js,.vue resources/js",
  "format": "prettier --write \"resources/js/**/*.{js,vue}\""
}

Настройка отладки



Для удобной отладки Vue-компонентов установите Vue DevTools - расширение для браузера. Оно позволяет исследовать дерево компонентов, их состояние и отслеживать события. Также полезно настроить доступ к Laravel Debug Bar, который помогает анализировать SQL-запросы, использование памяти и время загрузки:

Bash
1
composer require barryvdh/laravel-debugbar --dev
Laravel Debugbar автоматически отключается в продакшен-окружении, что обеспечивает безопасность и производительность.

Настройка окружения для команды



Если над проектом работает команда разработчиков, полезно добавить Docker-конфигурацию. Laravel предлагает инструмент Sail для быстрой настройки Docker-окружения:

Bash
1
2
composer require laravel/sail --dev
php artisan sail:install
После установки запустите контейнеры:

Bash
1
./vendor/bin/sail up
Sail предоставляет предварительно настроенное окружение с PHP, MySQL, Redis и другими сервисами, необходимыми для Laravel-приложения.

Архитектура приложения



Грамотно спроектированная архитектура — ключевой фактор успеха любого веб-приложения. Объединение Laravel и Vue.js требует продуманного подхода к структуре проекта, разделению ответственности и организации взаимодействия между клиентской и серверной частями.

Бэкенд на Laravel: API и обработка данных



В связке с Vue.js Laravel обычно выступает в роли API-сервера. Такой подход позволяет чётко разграничить ответственность: бэкенд занимается хранением и обработкой данных, взаимодействием с базой данных, валидацией входящих запросов и бизнес-логикой. Структура API-контроллеров Laravel может выглядеть так:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
 
namespace App\Http\Controllers\API;
 
use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;
use App\Http\Resources\TaskResource;
 
class TaskController extends Controller
{
    public function index()
    {
        return TaskResource::collection(Task::all());
    }
 
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'required|in:pending,in_progress,completed'
        ]);
 
        $task = Task::create($validated);
        return new TaskResource($task);
    }
 
    public function show(Task $task)
    {
        return new TaskResource($task);
    }
 
    public function update(Request $request, Task $task)
    {
        $validated = $request->validate([
            'name' => 'sometimes|string|max:255',
            'description' => 'nullable|string',
            'status' => 'sometimes|in:pending,in_progress,completed'
        ]);
 
        $task->update($validated);
        return new TaskResource($task);
    }
 
    public function destroy(Task $task)
    {
        $task->delete();
        return response()->json(null, 204);
    }
}
Для структурирования данных при отправке клиенту используются Resource-классы, которые преобразуют модели Eloquent в JSON-формат:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
 
namespace App\Http\Resources;
 
use Illuminate\Http\Resources\Json\JsonResource;
 
class TaskResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'status' => $this->status,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at
        ];
    }
}
Маршруты API в Laravel определяются в файле routes/api.php:

PHP
1
2
3
Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('tasks', TaskController::class);
});

Использование Laravel Sanctum для API-аутентификации



Для защиты API-маршрутов можно использовать Laravel Sanctum — легковесное решение для аутентификации SPA. Sanctum позволяет создавать токены доступа без необходимости использования полноценного OAuth-сервера.

Настройка Sanctum включает несколько шагов:

1. Установка пакета:
Bash
1
composer require laravel/sanctum
2. Публикация конфигурации и запуск миграций:
Bash
1
2
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
3. Добавление Sanctum middleware в app/Http/Kernel.php:
PHP
1
2
3
4
5
'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],
4. Обновление моделей пользователя:
PHP
1
2
3
4
5
6
7
use Laravel\Sanctum\HasApiTokens;
 
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    // ...
}
Теперь можно создать маршруты для аутентификации:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Route::post('/login', function (Request $request) {
    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required'
    ]);
 
    if (!Auth::attempt($credentials)) {
        return response()->json([
            'message' => 'Неверные учетные данные'
        ], 401);
    }
 
    $user = User::where('email', $request->email)->first();
    $token = $user->createToken('auth-token')->plainTextToken;
 
    return response()->json([
        'token' => $token,
        'user' => $user
    ]);
});
 
Route::middleware('auth:sanctum')->post('/logout', function (Request $request) {
    $request->user()->currentAccessToken()->delete();
    return response()->json(['message' => 'Выход выполнен успешно']);
});

Фронтенд на Vue.js: компоненты и маршрутизация



На стороне клиента приложение строится на основе компонентной структуры Vue.js. Главное преимущество такого подхода — возможность создавать переиспользуемые элементы интерфейса и разделять код на логические части.
Типичная структура директорий фронтенд-части может выглядеть так:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
resources/js/
├── app.js
├── components/
│   ├── App.vue
│   ├── common/
│   │   ├── Button.vue
│   │   ├── Card.vue
│   │   └── ...
│   ├── layout/
│   │   ├── Header.vue
│   │   ├── Sidebar.vue
│   │   └── ...
│   └── pages/
│       ├── Home.vue
│       ├── TaskList.vue
│       └── ...
├── router/
│   └── index.js
├── store/
│   ├── index.js
│   └── modules/
│       ├── auth.js
│       ├── tasks.js
│       └── ...
└── utils/
    ├── api.js
    └── helpers.js
Компонент страницы со списком задач может выглядеть так:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
  <div class="task-list">
    <h1>Список задач</h1>
    <div v-if="loading">Загрузка...</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else>
      <task-item 
        v-for="task in tasks" 
        :key="task.id" 
        :task="task"
        @delete="deleteTask"
        @update="updateTask"
      />
    </div>
    <task-form @submit="createTask" />
  </div>
</template>
 
<script>
import TaskItem from '@/components/TaskItem.vue'
import TaskForm from '@/components/TaskForm.vue'
import api from '@/utils/api'
 
export default {
  components: {
    TaskItem,
    TaskForm
  },
  data() {
    return {
      tasks: [],
      loading: true,
      error: null
    }
  },
  mounted() {
    this.fetchTasks()
  },
  methods: {
    async fetchTasks() {
      try {
        const response = await api.get('/tasks')
        this.tasks = response.data.data
        this.loading = false
      } catch (err) {
        this.error = 'Ошибка при загрузке задач'
        this.loading = false
      }
    },
    // другие методы для работы с задачами
  }
}
</script>
Для навигации между страницами используется Vue Router, который мы настроили в предыдущей главе.

Взаимодействие через Axios



Для взаимодействия фронтенда с бэкендом используется библиотека Axios. Для удобства работы с API можно создать специальный утилитарный файл:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// resources/js/utils/api.js
import axios from 'axios'
import router from '@/router'
import store from '@/store'
 
const api = axios.create({
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  }
})
 
// Добавление токена к запросам
api.interceptors.request.use(config => {
  const token = store.getters['auth/token']
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})
 
// Обработка ответов
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response && error.response.status === 401) {
      store.dispatch('auth/logout')
      router.push('/login')
    }
    return Promise.reject(error)
  }
)
 
export default api
Этот модуль устанавливает базовый URL для API-запросов, добавляет необходимые заголовки и настраивает перехватчики для автоматического добавления токена аутентификации к запросам и обработки ответов сервера.

Разделение бизнес-логики между фронтендом и бэкендом



Один из ключевых вопросов при проектировании приложения — где именно должна располагаться бизнес-логика. На практике оптимальным решением часто становится гибридный подход с разумным распределением ответственности.

На стороне Laravel размещается логика, связанная с:
  • Валидацией входных данных.
  • Обработкой сложных бизнес-правил.
  • Операциями с базой данных.
  • Безопасностью и авторизацией.
  • Долгосрочным хранением данных.

На стороне Vue.js обычно располагается:
  • Предварительная валидация форм.
  • Интерактивная фильтрация и сортировка данных.
  • Логика пользовательского интерфейса.
  • Кэширование часто используемых данных.
  • Управление состоянием приложения.

Рассмотрим конкретный пример. При создании задачи валидация полей может выполняться как на фронтенде (для мгновенной обратной связи), так и на бэкенде (для обеспечения надежности):

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<!-- TaskForm.vue -->
<template>
  <form @submit.prevent="submitForm">
    <div class="form-group">
      <label for="name">Название</label>
      <input
        id="name"
        v-model="form.name"
        class="form-control"
        :class="{ 'is-invalid': errors.name }"
      />
      <div v-if="errors.name" class="invalid-feedback">{{ errors.name }}</div>
    </div>
    
    <button type="submit" :disabled="isSubmitting">Сохранить</button>
  </form>
</template>
 
<script>
export default {
  data() {
    return {
      form: {
        name: '',
        description: '',
        status: 'pending'
      },
      errors: {},
      isSubmitting: false
    }
  },
  methods: {
    validateForm() {
      this.errors = {}
      
      if (!this.form.name) {
        this.errors.name = 'Название задачи обязательно'
      } else if (this.form.name.length > 255) {
        this.errors.name = 'Название не должно превышать 255 символов'
      }
      
      return Object.keys(this.errors).length === 0
    },
    
    async submitForm() {
      if (!this.validateForm()) return
      
      this.isSubmitting = true
      
      try {
        await api.post('/tasks', this.form)
        this.$emit('created')
        this.resetForm()
      } catch (error) {
        if (error.response && error.response.status === 422) {
          this.errors = error.response.data.errors
        } else {
          console.error('Ошибка при создании задачи', error)
        }
      } finally {
        this.isSubmitting = false
      }
    },
    
    resetForm() {
      this.form = {
        name: '',
        description: '',
        status: 'pending'
      }
      this.errors = {}
    }
  }
}
</script>

Стратегии кэширования данных между клиентом и сервером



Кэширование — важный аспект производительности современных веб-приложений. В связке Laravel и Vue.js можно реализовать многоуровневое кэширование:

1. Серверное кэширование:
Laravel предлагает гибкую систему кэширования с поддержкой разных драйверов (Redis, Memcached, файловая система). Например, кэширование результатов запросов:

PHP
1
2
3
4
5
6
   public function index()
   {
       return Cache::remember('tasks.all', 60 * 5, function () {
           return TaskResource::collection(Task::all());
       });
   }
2. HTTP-кэширование:
Можно использовать заголовки HTTP для управления кэшированием на уровне браузера:

PHP
1
2
3
4
5
6
   public function show(Task $task)
   {
       return (new TaskResource($task))
           ->response()
           ->header('Cache-Control', 'public, max-age=300');
   }
3. Клиентское кэширование:
Во Vue.js можно реализовать локальное хранение данных с помощью Vuex и localStorage:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
   // store/modules/tasks.js
   import api from '@/utils/api'
 
   export default {
     namespaced: true,
     
     state: {
       tasks: [],
       lastFetched: null
     },
     
     getters: {
       isStale: state => {
         if (!state.lastFetched) return true
         const fifteenMinutes = 15 * 60 * 1000
         return Date.now() - state.lastFetched > fifteenMinutes
       }
     },
     
     mutations: {
       SET_TASKS(state, tasks) {
         state.tasks = tasks
         state.lastFetched = Date.now()
       }
     },
     
     actions: {
       async fetchTasks({ commit, state, getters }) {
         // Используем кэшированные данные, если они не устарели
         if (state.tasks.length > 0 && !getters.isStale) {
           return state.tasks
         }
         
         const response = await api.get('/tasks')
         commit('SET_TASKS', response.data.data)
         return response.data.data
       },
       
       // Сохраняем задачи в localStorage при выходе из приложения
       saveToLocalStorage({ state }) {
         localStorage.setItem('tasks', JSON.stringify({
           data: state.tasks,
           timestamp: state.lastFetched
         }))
       },
       
       // Восстанавливаем задачи из localStorage при запуске
       loadFromLocalStorage({ commit }) {
         const cached = localStorage.getItem('tasks')
         if (cached) {
           const { data, timestamp } = JSON.parse(cached)
           commit('SET_TASKS', data)
           state.lastFetched = timestamp
         }
       }
     }
   }

Организация многоуровневой архитектуры



Для сложных приложений эффективным решением становится многоуровневая архитектура, где выделяются четкие слои с разными ответственностями:

1. Презентационный слой (Vue.js компоненты):
Отвечает за отображение интерфейса и взаимодействие с пользователем.
2. Слой состояния (Vuex):
Управляет состоянием приложения, обеспечивает централизованное хранилище данных.
3. Сервисный слой (сервисы Vue.js):
Содержит бизнес-логику на стороне клиента и инкапсулирует взаимодействие с API.
4. API-слой (Laravel контроллеры):
Обрабатывает HTTP-запросы, валидирует данные и возвращает правильные HTTP-ответы.
5. Слой бизнес-логики (Laravel сервисы):
Содержит бизнес-правила и логику обработки данных, не зависящую от способа доставки.
6. Слой доступа к данным (Laravel репозитории):
Инкапсулирует взаимодействие с базой данных и другими источниками данных.
7. Слой хранения данных (база данных):
Непосредственно хранит данные приложения.

Такое разделение позволяет достичь высокой модульности, улучшить тестируемость и упростить поддержку приложения.
Например, можно создать сервисный слой в Laravel:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// App/Services/TaskService.php
class TaskService
{
    protected $taskRepository;
    
    public function __construct(TaskRepository $taskRepository)
    {
        $this->taskRepository = $taskRepository;
    }
    
    public function getAllTasks()
    {
        return $this->taskRepository->getAll();
    }
    
    public function completeTask(Task $task)
    {
        if ($task->status === 'completed') {
            throw new BusinessLogicException('Задача уже завершена');
        }
        
        $task->status = 'completed';
        $task->completed_at = now();
        
        return $this->taskRepository->save($task);
    }
}
И соответствующий репозиторий:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// App/Repositories/TaskRepository.php
class TaskRepository
{
    public function getAll()
    {
        return Task::all();
    }
    
    public function getById($id)
    {
        return Task::findOrFail($id);
    }
    
    public function save(Task $task)
    {
        $task->save();
        return $task;
    }
}

Практическая реализация



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

Создание моделей и миграций



Начнем реализацию нашего приложения с создания моделей данных и соответствующих миграций. Для менеджера задач нам понадобится как минимум модель Task. Создадим её с помощью artisan-команды:

Bash
1
php artisan make:model Task -m
Флаг -m автоматически создаст файл миграции для этой модели. Теперь настроим структуру таблицы в миграции:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
 
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
class CreateTasksTable extends Migration
{
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->timestamp('completed_at')->nullable();
            $table->timestamps();
        });
    }
 
    public function down()
    {
        Schema::dropIfExists('tasks');
    }
}
Определим модель Task с необходимыми свойствами:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Task extends Model
{
    use HasFactory;
 
    protected $fillable = [
        'name', 'description', 'status', 'user_id', 'completed_at'
    ];
 
    protected $casts = [
        'completed_at' => 'datetime',
    ];
 
    public function user()
    {
        return $this->belongsTo(User::class);
    }
    
    public function scopeForUser($query, $userId)
    {
        return $query->where('user_id', $userId);
    }
}
Запустим миграции командой:

Bash
1
php artisan migrate

Разработка API-контроллеров



Для взаимодействия с фронтендом создадим RESTful API-контроллер:

Bash
1
php artisan make:controller API/TaskController --api
Теперь реализуем логику обработки запросов:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
 
namespace App\Http\Controllers\API;
 
use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;
use App\Http\Resources\TaskResource;
use App\Http\Requests\TaskRequest;
 
class TaskController extends Controller
{
    public function index(Request $request)
    {
        $tasks = Task::forUser($request->user()->id)
            ->when($request->status, function($query, $status) {
                return $query->where('status', $status);
            })
            ->orderBy('created_at', 'desc')
            ->get();
            
        return TaskResource::collection($tasks);
    }
 
    public function store(TaskRequest $request)
    {
        $task = Task::create([
            'name' => $request->name,
            'description' => $request->description,
            'status' => $request->status ?? 'pending',
            'user_id' => $request->user()->id
        ]);
 
        return new TaskResource($task);
    }
 
    public function show(Task $task)
    {
        $this->authorize('view', $task);
        return new TaskResource($task);
    }
 
    public function update(TaskRequest $request, Task $task)
    {
        $this->authorize('update', $task);
        
        $task->update($request->validated());
        
        if ($request->status === 'completed' && !$task->completed_at) {
            $task->completed_at = now();
            $task->save();
        }
 
        return new TaskResource($task);
    }
 
    public function destroy(Task $task)
    {
        $this->authorize('delete', $task);
        
        $task->delete();
        
        return response()->json(null, 204);
    }
}
Для валидации запросов создадим отдельный класс запроса:

Bash
1
php artisan make:request TaskRequest
PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
 
namespace App\Http\Requests;
 
use Illuminate\Foundation\Http\FormRequest;
 
class TaskRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }
 
    public function rules()
    {
        $rules = [
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'sometimes|in:pending,in_progress,completed',
        ];
        
        // Для обновления делаем поля опциональными
        if ($this->isMethod('patch') || $this->isMethod('put')) {
            $rules['name'] = 'sometimes|required|string|max:255';
        }
        
        return $rules;
    }
}
Создадим также Resource для форматирования ответов API:

Bash
1
php artisan make:resource TaskResource
PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
 
namespace App\Http\Resources;
 
use Illuminate\Http\Resources\Json\JsonResource;
 
class TaskResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'status' => $this->status,
            'completed_at' => $this->completed_at,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}
Определим маршруты API в файле routes/api.php:

PHP
1
2
3
Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('tasks', \App\Http\Controllers\API\TaskController::class);
});

Обработка ошибок и валидация



Laravel обеспечивает удобный способ обработки ошибок через исключения. Настроим глобальную обработку исключений в app/Exceptions/Handler.php:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
 
namespace App\Exceptions;
 
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
 
class Handler extends ExceptionHandler
{
    // ...
    
    public function render($request, Throwable $exception)
    {
        if ($request->expectsJson()) {
            if ($exception instanceof ValidationException) {
                return response()->json([
                    'message' => 'Ошибка валидации данных',
                    'errors' => $exception->errors()
                ], 422);
            }
            
            if ($exception instanceof NotFoundHttpException) {
                return response()->json([
                    'message' => 'Ресурс не найден'
                ], 404);
            }
            
            if ($exception instanceof \Illuminate\Auth\Access\AuthorizationException) {
                return response()->json([
                    'message' => 'Доступ запрещен'
                ], 403);
            }
        }
        
        return parent::render($request, $exception);
    }
}
На стороне Vue.js также важно обеспечить валидацию форм до отправки данных на сервер. Для этого создадим компонент с валидацией:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<template>
  <form @submit.prevent="submitForm" class="task-form">
    <div class="form-group">
      <label for="name">Название задачи</label>
      <input
        id="name"
        v-model="form.name"
        class="form-control"
        :class="{ 'is-invalid': errors.name }"
      />
      <div v-if="errors.name" class="invalid-feedback">{{ errors.name[0] }}</div>
    </div>
    
    <div class="form-group">
      <label for="description">Описание</label>
      <textarea
        id="description"
        v-model="form.description"
        class="form-control"
        :class="{ 'is-invalid': errors.description }"
      ></textarea>
      <div v-if="errors.description" class="invalid-feedback">{{ errors.description[0] }}</div>
    </div>
    
    <div class="form-group">
      <label for="status">Статус</label>
      <select
        id="status"
        v-model="form.status"
        class="form-control"
        :class="{ 'is-invalid': errors.status }"
      >
        <option value="pending">В ожидании</option>
        <option value="in_progress">В процессе</option>
        <option value="completed">Завершено</option>
      </select>
      <div v-if="errors.status" class="invalid-feedback">{{ errors.status[0] }}</div>
    </div>
    
    <button type="submit" class="btn btn-primary" :disabled="isSubmitting">
      {{ isSubmitting ? 'Сохранение...' : 'Сохранить' }}
    </button>
  </form>
</template>
 
<script>
import { ref, reactive } from 'vue'
import api from '@/utils/api'
 
export default {
  props: {
    task: {
      type: Object,
      default: null
    }
  },
  
  setup(props, { emit }) {
    const form = reactive({
      name: props.task?.name || '',
      description: props.task?.description || '',
      status: props.task?.status || 'pending'
    })
    
    const errors = reactive({})
    const isSubmitting = ref(false)
    
    const validateForm = () => {
      errors.name = !form.name ? ['Название задачи обязательно'] : null
      
      return !errors.name
    }
    
    const submitForm = async () => {
      if (!validateForm()) return
      
      isSubmitting.value = true
      
      try {
        let response
        
        if (props.task) {
          response = await api.put(`/tasks/${props.task.id}`, form)
          emit('updated', response.data.data)
        } else {
          response = await api.post('/tasks', form)
          emit('created', response.data.data)
        }
        
        // Сброс формы после успешного создания
        if (!props.task) {
          form.name = ''
          form.description = ''
          form.status = 'pending'
        }
      } catch (error) {
        if (error.response && error.response.status === 422) {
          Object.assign(errors, error.response.data.errors)
        } else {
          console.error('Ошибка при сохранении задачи', error)
        }
      } finally {
        isSubmitting.value = false
      }
    }
    
    return {
      form,
      errors,
      isSubmitting,
      submitForm
    }
  }
}
</script>
Такой подход обеспечивает двойную валидацию — как на стороне клиента для мгновенной обратной связи, так и на стороне сервера для надежности.

Компоненты Vue.js и их связывание



После настройки бэкенда и моделей давайте разработаем компоненты Vue.js для отображения задач. Начнем с компонента для отображения отдельной задачи:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<template>
<div class="task-item" :class="statusClass">
  <div class="task-header">
    <h3>{{ task.name }}</h3>
    <div class="task-controls">
      <button @click="editTask" class="btn btn-sm btn-outline-primary">
        <i class="fas fa-edit"></i>
      </button>
      <button @click="confirmDelete" class="btn btn-sm btn-outline-danger">
        <i class="fas fa-trash"></i>
      </button>
    </div>
  </div>
  
  <p v-if="task.description">{{ task.description }}</p>
  
  <div class="task-footer">
    <div class="task-status">
      <span class="status-badge">{{ statusText }}</span>
    </div>
    <div class="task-date">
      {{ formatDate(task.created_at) }}
    </div>
  </div>
</div>
</template>
 
<script>
import { computed } from 'vue'
import { formatDistanceToNow } from 'date-fns'
import { ru } from 'date-fns/locale'
 
export default {
props: {
  task: {
    type: Object,
    required: true
  }
},
 
setup(props, { emit }) {
  const statusClass = computed(() => `status-${props.task.status}`)
  
  const statusText = computed(() => {
    const statuses = {
      'pending': 'В ожидании',
      'in_progress': 'В процессе',
      'completed': 'Завершено'
    }
    return statuses[props.task.status] || props.task.status
  })
  
  const formatDate = (date) => {
    return formatDistanceToNow(new Date(date), { addSuffix: true, locale: ru })
  }
  
  const editTask = () => {
    emit('edit', props.task)
  }
  
  const confirmDelete = () => {
    if (confirm('Вы уверены, что хотите удалить эту задачу?')) {
      emit('delete', props.task.id)
    }
  }
  
  return {
    statusClass,
    statusText,
    formatDate,
    editTask,
    confirmDelete
  }
}
}
</script>
 
<style scoped>
.task-item {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 15px;
  margin-bottom: 15px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
 
.status-pending {
  border-left: 4px solid #ffc107;
}
 
.status-in_progress {
  border-left: 4px solid #17a2b8;
}
 
.status-completed {
  border-left: 4px solid #28a745;
}
</style>
Теперь создадим родительский компонент, который будет отображать список задач и управлять ими:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<template>
<div class="tasks-container">
  <div class="tasks-header">
    <h1>Мои задачи</h1>
    <div class="filters">
      <select v-model="currentFilter" class="form-control">
        <option value="all">Все задачи</option>
        <option value="pending">В ожидании</option>
        <option value="in_progress">В процессе</option>
        <option value="completed">Завершенные</option>
      </select>
    </div>
  </div>
  
  <div v-if="loading" class="loader">
    <div class="spinner-border" role="status">
      <span class="sr-only">Загрузка...</span>
    </div>
  </div>
  
  <div v-else-if="error" class="alert alert-danger">
    {{ error }}
  </div>
  
  <div v-else-if="filteredTasks.length === 0" class="no-tasks">
    <p>Задачи не найдены</p>
    <button @click="showForm = true" class="btn btn-primary">
      Создать первую задачу
    </button>
  </div>
  
  <div v-else class="task-list">
    <task-item
      v-for="task in filteredTasks"
      :key="task.id"
      :task="task"
      @edit="startEditing"
      @delete="deleteTask"
    />
  </div>
  
  <button v-if="filteredTasks.length > 0" @click="showForm = true" class="btn btn-primary add-task-btn">
    Добавить задачу
  </button>
  
  <div v-if="showForm" class="task-form-modal">
    <div class="task-form-container">
      <h2>{{ editingTask ? 'Редактировать задачу' : 'Новая задача' }}</h2>
      <task-form
        :task="editingTask"
        @created="taskCreated"
        @updated="taskUpdated"
        @cancel="cancelForm"
      />
    </div>
  </div>
</div>
</template>
 
<script>
import { ref, computed, onMounted } from 'vue'
import TaskItem from '@/components/TaskItem.vue'
import TaskForm from '@/components/TaskForm.vue'
import api from '@/utils/api'
 
export default {
components: {
  TaskItem,
  TaskForm
},
 
setup() {
  const tasks = ref([])
  const loading = ref(true)
  const error = ref(null)
  const showForm = ref(false)
  const editingTask = ref(null)
  const currentFilter = ref('all')
  
  const fetchTasks = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await api.get('/tasks')
      tasks.value = response.data.data
    } catch (err) {
      error.value = 'Не удалось загрузить задачи. Пожалуйста, попробуйте позже.'
      console.error(err)
    } finally {
      loading.value = false
    }
  }
  
  const filteredTasks = computed(() => {
    if (currentFilter.value === 'all') {
      return tasks.value
    }
    
    return tasks.value.filter(task => task.status === currentFilter.value)
  })
  
  const startEditing = (task) => {
    editingTask.value = task
    showForm.value = true
  }
  
  const cancelForm = () => {
    showForm.value = false
    editingTask.value = null
  }
  
  const taskCreated = (newTask) => {
    tasks.value.unshift(newTask)
    showForm.value = false
  }
  
  const taskUpdated = (updatedTask) => {
    const index = tasks.value.findIndex(t => t.id === updatedTask.id)
    if (index !== -1) {
      tasks.value[index] = updatedTask
    }
    showForm.value = false
    editingTask.value = null
  }
  
  const deleteTask = async (taskId) => {
    try {
      await api.delete(`/tasks/${taskId}`)
      tasks.value = tasks.value.filter(t => t.id !== taskId)
    } catch (err) {
      error.value = 'Не удалось удалить задачу'
      console.error(err)
    }
  }
  
  onMounted(fetchTasks)
  
  return {
    tasks,
    loading,
    error,
    showForm,
    editingTask,
    currentFilter,
    filteredTasks,
    startEditing,
    cancelForm,
    taskCreated,
    taskUpdated,
    deleteTask
  }
}
}
</script>

Аутентификация и авторизация



Для полноценного приложения необходимо реализовать систему аутентификации. Создадим компоненты для регистрации и входа:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<template>
<div class="auth-form">
  <h2>{{ isLogin ? 'Вход' : 'Регистрация' }}</h2>
  
  <form @submit.prevent="submitForm">
    <div v-if="!isLogin" class="form-group">
      <label for="name">Имя</label>
      <input
        id="name"
        v-model="form.name"
        type="text"
        class="form-control"
        :class="{ 'is-invalid': errors.name }"
        required
      />
      <div v-if="errors.name" class="invalid-feedback">{{ errors.name[0] }}</div>
    </div>
    
    <div class="form-group">
      <label for="email">Email</label>
      <input
        id="email"
        v-model="form.email"
        type="email"
        class="form-control"
        :class="{ 'is-invalid': errors.email }"
        required
      />
      <div v-if="errors.email" class="invalid-feedback">{{ errors.email[0] }}</div>
    </div>
    
    <div class="form-group">
      <label for="password">Пароль</label>
      <input
        id="password"
        v-model="form.password"
        type="password"
        class="form-control"
        :class="{ 'is-invalid': errors.password }"
        required
      />
      <div v-if="errors.password" class="invalid-feedback">{{ errors.password[0] }}</div>
    </div>
    
    <div v-if="!isLogin" class="form-group">
      <label for="password_confirmation">Подтверждение пароля</label>
      <input
        id="password_confirmation"
        v-model="form.password_confirmation"
        type="password"
        class="form-control"
        required
      />
    </div>
    
    <button type="submit" class="btn btn-primary btn-block" :disabled="isSubmitting">
      {{ isSubmitting ? 'Подождите...' : (isLogin ? 'Войти' : 'Зарегистрироваться') }}
    </button>
    
    <div v-if="formError" class="alert alert-danger mt-3">
      {{ formError }}
    </div>
  </form>
  
  <div class="auth-toggle">
    <p>
      {{ isLogin ? 'Нет аккаунта?' : 'Уже есть аккаунт?' }}
      <a href="#" @click.prevent="toggleAuthMode">
        {{ isLogin ? 'Зарегистрироваться' : 'Войти' }}
      </a>
    </p>
  </div>
</div>
</template>

Продвинутые техники



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

Реактивное управление состоянием с Vuex



Для управления состоянием в масштабных Vue.js приложениях идеально подходит Vuex. Это паттерн управления состоянием и библиотека, которые обеспечивают централизованное хранилище данных для всех компонентов приложения. Установка Vuex:

Bash
1
npm install vuex@next
Создадим базовую структуру хранилища в resources/js/store/index.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
import { createStore } from 'vuex'
import auth from './modules/auth'
import tasks from './modules/tasks'
 
export default createStore({
  modules: {
    auth,
    tasks
  }
})
Модуль для работы с задачами resources/js/store/modules/tasks.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import api from '@/utils/api'
 
export default {
  namespaced: true,
  
  state: () => ({
    tasks: [],
    loading: false,
    error: null
  }),
  
  getters: {
    pendingTasks: state => state.tasks.filter(task => task.status === 'pending'),
    inProgressTasks: state => state.tasks.filter(task => task.status === 'in_progress'),
    completedTasks: state => state.tasks.filter(task => task.status === 'completed')
  },
  
  mutations: {
    SET_LOADING(state, status) {
      state.loading = status
    },
    SET_ERROR(state, error) {
      state.error = error
    },
    SET_TASKS(state, tasks) {
      state.tasks = tasks
    },
    ADD_TASK(state, task) {
      state.tasks.unshift(task)
    },
    UPDATE_TASK(state, updatedTask) {
      const index = state.tasks.findIndex(t => t.id === updatedTask.id)
      if (index !== -1) {
        state.tasks.splice(index, 1, updatedTask)
      }
    },
    REMOVE_TASK(state, taskId) {
      state.tasks = state.tasks.filter(t => t.id !== taskId)
    }
  },
  
  actions: {
    async fetchTasks({ commit }) {
      commit('SET_LOADING', true)
      commit('SET_ERROR', null)
      
      try {
        const response = await api.get('/tasks')
        commit('SET_TASKS', response.data.data)
      } catch (error) {
        commit('SET_ERROR', 'Не удалось загрузить задачи')
        console.error(error)
      } finally {
        commit('SET_LOADING', false)
      }
    },
    
    async createTask({ commit }, taskData) {
      try {
        const response = await api.post('/tasks', taskData)
        commit('ADD_TASK', response.data.data)
        return response.data.data
      } catch (error) {
        console.error(error)
        throw error
      }
    },
    
    async updateTask({ commit }, { id, data }) {
      try {
        const response = await api.put(`/tasks/${id}`, data)
        commit('UPDATE_TASK', response.data.data)
        return response.data.data
      } catch (error) {
        console.error(error)
        throw error
      }
    },
    
    async deleteTask({ commit }, taskId) {
      try {
        await api.delete(`/tasks/${taskId}`)
        commit('REMOVE_TASK', taskId)
      } catch (error) {
        console.error(error)
        throw error
      }
    }
  }
}
После создания хранилища нужно подключить его к приложению в resources/js/app.js:

JavaScript
1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './components/App.vue'
import router from './router'
import store from './store'
 
createApp(App)
  .use(router)
  .use(store)
  .mount('#app')
Теперь в компонентах можно использовать хранилище для управления состоянием:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script>
import { computed, onMounted } from 'vue'
import { useStore } from 'vuex'
 
export default {
  setup() {
    const store = useStore()
    
    const tasks = computed(() => store.state.tasks.tasks)
    const loading = computed(() => store.state.tasks.loading)
    const error = computed(() => store.state.tasks.error)
    
    const pendingTasks = computed(() => store.getters['tasks/pendingTasks'])
    
    onMounted(() => {
      store.dispatch('tasks/fetchTasks')
    })
    
    const createTask = (taskData) => {
      return store.dispatch('tasks/createTask', taskData)
    }
    
    return {
      tasks,
      loading,
      error,
      pendingTasks,
      createTask
    }
  }
}
</script>

Использование Laravel Horizon для управления очередями



Для обработки фоновых задач, таких как отправка электронных писем, генерация отчетов или обработка файлов, Laravel предоставляет систему очередей. Laravel Horizon — это красивая панель мониторинга и конфигурации для очередей Laravel, работающих на Redis. Установка Horizon:

Bash
1
2
composer require laravel/horizon
php artisan horizon:install
После установки вы получите панель управления, доступную по пути /horizon.

Для использования очередей создадим задачу (Job):

Bash
1
php artisan make:job ProcessTaskReminders
PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
 
namespace App\Jobs;
 
use App\Models\Task;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use App\Mail\TaskReminder;
 
class ProcessTaskReminders implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        // Находим все просроченные задачи
        $tasks = Task::where('status', '!=', 'completed')
            ->where('due_date', '<', now()->addDay())
            ->get();
            
        foreach ($tasks as $task) {
            // Отправляем напоминание пользователю
            Mail::to($task->user)->send(new TaskReminder($task));
        }
    }
}
Затем планируем выполнение задачи в app/Console/Kernel.php:

PHP
1
2
3
4
protected function schedule(Schedule $schedule)
{
    $schedule->job(new ProcessTaskReminders)->daily();
}
Для запуска обработчика очередей в рабочей среде используйте:

Bash
1
php artisan horizon

Оптимизация производительности



Оптимизация производительности является критически важным аспектом при разработке веб-приложений. В контексте связки Laravel и Vue.js можно применить несколько эффективных подходов. Для оптимизации фронтенда при сборке продакшн-версии приложения используйте:

Bash
1
npm run production
Этот скрипт запускает Laravel Mix с оптимизациями, включая минификацию и сжатие ресурсов. Для дополнительных улучшений можно настроить разделение кода (code splitting):

JavaScript
1
2
3
4
5
// webpack.mix.js
mix.js('resources/js/app.js', 'public/js')
   .vue()
   .extract(['vue', 'vuex', 'vue-router', 'axios'])
   .version();
Эта конфигурация выделяет часто используемые библиотеки в отдельный vendor-файл, что позволяет браузеру кэшировать их независимо от основного кода приложения. На стороне Laravel важно оптимизировать запросы к базе данных. Используйте Eloquent с умом, избегая проблемы N+1 запросов:

PHP
1
2
3
4
5
6
7
8
9
10
11
// Плохо: вызовет N+1 запросов
$tasks = Task::all();
foreach ($tasks as $task) {
    echo $task->user->name;
}
 
// Хорошо: загрузит пользователей за один запрос
$tasks = Task::with('user')->get();
foreach ($tasks as $task) {
    echo $task->user->name;
}
Для кэширования часто запрашиваемых данных используйте Redis:

PHP
1
2
3
$tasks = Cache::remember('user.tasks.' . $userId, 3600, function () use ($userId) {
    return Task::where('user_id', $userId)->get();
});

Тестирование приложения



Тестирование – неотъемлемая часть процесса разработки качественного программного обеспечения. Laravel и Vue.js предоставляют мощные инструменты для тестирования. Для тестирования API Laravel используйте встроенные возможности фреймворка:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
 
namespace Tests\Feature;
 
use App\Models\Task;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class TaskApiTest extends TestCase
{
    use RefreshDatabase;
 
    public function test_user_can_get_their_tasks()
    {
        $user = User::factory()->create();
        $tasks = Task::factory()->count(3)->create(['user_id' => $user->id]);
        
        $response = $this->actingAs($user)
                         ->getJson('/api/tasks');
        
        $response->assertStatus(200)
                 ->assertJsonCount(3, 'data');
    }
    
    public function test_user_can_create_task()
    {
        $user = User::factory()->create();
        
        $response = $this->actingAs($user)
                         ->postJson('/api/tasks', [
                             'name' => 'Test Task',
                             'description' => 'Test Description',
                             'status' => 'pending'
                         ]);
        
        $response->assertStatus(201)
                 ->assertJson([
                     'data' => [
                         'name' => 'Test Task',
                         'status' => 'pending'
                     ]
                 ]);
                 
        $this->assertDatabaseHas('tasks', [
            'name' => 'Test Task',
            'user_id' => $user->id
        ]);
    }
}
Для тестирования компонентов Vue.js можно использовать Vue Test Utils:

Bash
1
npm install --save-dev @vue/test-utils@next jest vue-jest
Создайте файл конфигурации Jest `jest.config.js`:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
  testEnvironment: 'jsdom',
  moduleFileExtensions: [
    'js',
    'vue'
  ],
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '^.+\\.js$': 'babel-jest'
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/resources/js/$1'
  }
}
Пример теста для компонента:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { mount } from '@vue/test-utils'
import TaskItem from '@/components/TaskItem.vue'
 
describe('TaskItem.vue', () => {
  it('renders task name correctly', () => {
    const task = {
      id: 1,
      name: 'Test Task',
      status: 'pending',
      created_at: new Date().toISOString()
    }
    
    const wrapper = mount(TaskItem, {
      props: { task }
    })
    
    expect(wrapper.text()).toContain('Test Task')
  })
  
  it('emits delete event when delete button is clicked', async () => {
    const task = {
      id: 1,
      name: 'Test Task',
      status: 'pending',
      created_at: new Date().toISOString()
    }
    
    // Мокаем window.confirm
    window.confirm = jest.fn(() => true)
    
    const wrapper = mount(TaskItem, {
      props: { task }
    })
    
    await wrapper.find('.btn-outline-danger').trigger('click')
    
    expect(wrapper.emitted().delete).toBeTruthy()
    expect(wrapper.emitted().delete[0]).toEqual([1])
  })
})

Интеграция Laravel Echo для работы с WebSockets



Современные веб-приложения требуют мгновенной передачи данных между сервером и клиентом, что невозможно реализовать традиционными HTTP-запросами. Тут на помощь приходят WebSocket-соединения, а Laravel Echo – отличный инструмент для их реализации.

Для начала установим необходимые пакеты:

Bash
1
2
composer require pusher/pusher-php-server
npm install laravel-echo pusher-js
Laravel имеет встроенную систему событий, поддерживающую различные бродкаст-драйверы. Настроим конфигурацию в файле .env:

PHP
1
2
3
4
5
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your_app_id
PUSHER_APP_KEY=your_app_key
PUSHER_APP_SECRET=your_app_secret
PUSHER_APP_CLUSTER=eu
Также настроим драйвер Pusher в файле config/broadcasting.php:

PHP
1
2
3
4
5
6
7
8
9
10
'pusher' => [
    'driver' => 'pusher',
    'key' => env('PUSHER_APP_KEY'),
    'secret' => env('PUSHER_APP_SECRET'),
    'app_id' => env('PUSHER_APP_ID'),
    'options' => [
        'cluster' => env('PUSHER_APP_CLUSTER'),
        'encrypted' => true,
    ],
],
Теперь создадим событие, которое будет бродкастится при обновлении задачи:

Bash
1
php artisan make:event TaskUpdated
PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
 
namespace App\Events;
 
use App\Models\Task;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
 
class TaskUpdated implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;
 
    public $task;
 
    public function __construct(Task $task)
    {
        $this->task = $task;
    }
 
    public function broadcastOn()
    {
        return new PrivateChannel('tasks.' . $this->task->user_id);
    }
    
    public function broadcastWith()
    {
        return [
            'id' => $this->task->id,
            'name' => $this->task->name,
            'status' => $this->task->status,
            'updated_at' => $this->task->updated_at->toIso8601String()
        ];
    }
}
Теперь вызовем это событие в методе update TaskController'а:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function update(TaskRequest $request, Task $task)
{
    $this->authorize('update', $task);
    
    $task->update($request->validated());
    
    if ($request->status === 'completed' && !$task->completed_at) {
        $task->completed_at = now();
        $task->save();
    }
 
    // Бродкастим событие
    event(new TaskUpdated($task));
 
    return new TaskResource($task);
}
Также необходимо настроить маршруты для авторизации каналов в файле routes/channels.php:

PHP
1
2
3
Broadcast::channel('tasks.{userId}', function ($user, $userId) {
    return (int) $user->id === (int) $userId;
});
На стороне клиента инициализируем Laravel Echo в файле resources/js/bootstrap.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
 
window.Pusher = Pusher;
 
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    encrypted: true,
    forceTLS: true,
    authorizer: (channel, options) => {
        return {
            authorize: (socketId, callback) => {
                axios.post('/broadcasting/auth', {
                    socket_id: socketId,
                    channel_name: channel.name
                })
                .then(response => {
                    callback(false, response.data);
                })
                .catch(error => {
                    callback(true, error);
                });
            }
        };
    }
});
Теперь добавим прослушивание событий в компоненте Vue.js:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script>
import { onMounted, onUnmounted } from 'vue';
import { useStore } from 'vuex';
 
export default {
  setup() {
    const store = useStore();
    let echoChannel = null;
    
    onMounted(() => {
      // Подписываемся на приватный канал для текущего пользователя
      echoChannel = window.Echo.private(`tasks.${store.state.auth.user.id}`)
        .listen('TaskUpdated', (e) => {
          // Обновляем задачу в хранилище
          store.commit('tasks/UPDATE_TASK', e.task);
          
          // Показываем уведомление
          store.dispatch('notifications/add', {
            type: 'success',
            message: [INLINE]Задача "${e.task.name}" была обновлена[/INLINE]
          });
        });
    });
    
    onUnmounted(() => {
      // Отписываемся от канала при уничтожении компонента
      if (echoChannel) {
        echoChannel.unsubscribe();
      }
    });
    
    // Остальной код компонента
  }
}
</script>
Такая реализация позволяет мгновенно обновлять пользовательский интерфейс при изменении данных на сервере без дополнительных запросов. Особенно полезно это в многопользовательских системах, где несколько пользователей могут одновременно работать с одними и теми же данными.

Создание прогрессивного веб-приложения (PWA)



Прогрессивные веб-приложения (PWA) объединяют лучшие качества веб-сайтов и нативных приложений. Они работают в автономном режиме, поддерживают push-уведомления и могут быть установлены на домашний экран устройства. Создадим PWA на основе нашего приложения Laravel и Vue.js. Для начала установим нужные пакеты:

Bash
1
npm install workbox-webpack-plugin register-service-worker
Настроим webpack.mix.js для поддержки PWA:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const mix = require('laravel-mix');
const { GenerateSW } = require('workbox-webpack-plugin');
 
mix.js('resources/js/app.js', 'public/js')
   .vue()
   .sass('resources/sass/app.scss', 'public/css')
   .webpackConfig({
      plugins: [
         new GenerateSW({
            clientsClaim: true,
            skipWaiting: true,
            runtimeCaching: [
               {
                  urlPattern: new RegExp('^https://fonts.googleapis.com/'),
                  handler: 'StaleWhileRevalidate',
                  options: {
                     cacheName: 'google-fonts',
                     expiration: {
                        maxEntries: 30,
                        maxAgeSeconds: 60 * 60 * 24 * 30,
                     },
                  },
               },
               {
                  urlPattern: new RegExp('/api/tasks'),
                  handler: 'NetworkFirst',
                  options: {
                     cacheName: 'api-cache',
                     networkTimeoutSeconds: 10,
                     expiration: {
                        maxEntries: 50,
                        maxAgeSeconds: 60 * 5,
                     },
                  },
               },
            ],
         }),
      ],
   });
Создадим файл для регистрации сервис-воркера resources/js/registerServiceWorker.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { register } from 'register-service-worker';
 
if (process.env.NODE_ENV === 'production') {
  register(`/service-worker.js`, {
    ready() {
      console.log('App is being served from cache by a service worker.');
    },
    registered() {
      console.log('Service worker has been registered.');
    },
    cached() {
      console.log('Content has been cached for offline use.');
    },
    updatefound() {
      console.log('New content is downloading.');
    },
    updated() {
      console.log('New content is available; please refresh.');
      // Уведомим пользователя о необходимости обновить страницу
      alert('Доступна новая версия приложения. Пожалуйста, обновите страницу.');
    },
    offline() {
      console.log('No internet connection found. App is running in offline mode.');
    },
    error(error) {
      console.error('Error during service worker registration:', error);
    }
  });
}
Импортируем этот файл в resources/js/app.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue';
import App from './components/App.vue';
import router from './router';
import store from './store';
import './registerServiceWorker';
 
createApp(App)
  .use(router)
  .use(store)
  .mount('#app');
Создадим манифест веб-приложения public/manifest.json:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
  "name": "Task Manager",
  "short_name": "Tasks",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#4a5568",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
Не забудьте добавить ссылку на манифест в ваш шаблон resources/views/app.blade.php:

HTML5
1
2
3
4
5
6
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#4a5568">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Task Manager">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
Для улучшения опыта использования в офлайн-режиме, можно создать специальную страницу, которая будет отображаться при отсутствии интернет-соединения. Добавим компонент resources/js/components/OfflinePage.vue:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
  <div class="offline-container">
    <h1>Вы в офлайн-режиме</h1>
    <p>Интернет-соединение отсутствует. Некоторые функции могут быть недоступны.</p>
    <p>Вы всё еще можете просматривать кэшированные данные.</p>
    
    <router-link to="/" class="btn btn-primary">
      Вернуться на главную
    </router-link>
  </div>
</template>
 
<script>
export default {
  name: 'OfflinePage'
}
</script>
И добавим маршрут в resources/js/router/index.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
import OfflinePage from '../components/OfflinePage.vue'
 
const routes = [
  // Другие маршруты
  {
    path: '/offline',
    name: 'offline',
    component: OfflinePage
  }
]
С помощью этих настроек наше приложение будет работать как PWA, сохраняя данные для офлайн-использования и предоставляя возможность установки на домашний экран устройства.

CI/CD-пайплайны для автоматизации развертывания



Для автоматизации процессов тестирования, сборки и развертывания приложений на Laravel и Vue.js удобно использовать CI/CD-пайплайны. Рассмотрим настройку пайплайна с помощью GitHub Actions. Создадим файл .github/workflows/deploy.yml:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
name: Deploy Application
 
on:
  push:
    branches: [ main, master ]
 
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
        extensions: mbstring, dom, fileinfo, mysql
        coverage: xdebug
    
    - name: Install PHP dependencies
      run: composer install --prefer-dist --no-interaction
    
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
    
    - name: Install JS dependencies
      run: npm ci
    
    - name: Build assets
      run: npm run production
    
    - name: Prepare Laravel Application
      run: |
        cp .env.example .env
        php artisan key:generate
        php artisan config:cache
        php artisan route:cache
    
    - name: Run PHP tests
      run: php artisan test
    
    - name: Run JS tests
      run: npm test
    
    - name: Archive build artifacts
      uses: actions/upload-artifact@v2
      with:
        name: build
        path: |
          public/js
          public/css
          public/mix-manifest.json
 
  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: success()
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Download build artifacts
      uses: actions/download-artifact@v2
      with:
        name: build
        path: public
    
    - name: Deploy to production
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USERNAME }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /var/www/my-app
          git pull origin main
          composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
          php artisan migrate --force
          php artisan config:cache
          php artisan route:cache
          php artisan view:cache
          php artisan queue:restart
Для работы этого пайплайна необходимо добавить секреты в репозиторий GitHub:
SSH_HOST - IP-адрес или доменное имя сервера,
SSH_USERNAME - имя пользователя на сервере,
SSH_PRIVATE_KEY - приватный SSH-ключ для доступа к серверу.

Этот пайплайн выполняет несколько шагов:
1. Запускает тесты на бекенде и фронтенде.
2. Собирает ресурсы для продакшн-окружения.
3. При успешном выполнении предыдущих шагов, выполняет деплой на сервер.

Для более продвинутых сценариев, можно настроить отдельные окружения для разработки, тестирования и продакшена.

Решение проблем безопасности в гибридных приложениях



Безопасность – критически важный аспект веб-приложений. Приложения, построенные на Laravel и Vue.js, сталкиваются с уникальными проблемами безопасности, характерными для SPA-приложений.

Защита от CSRF-атак



Laravel автоматически защищает приложение от CSRF-атак, но в случае с SPA требуется особый подход. При использовании Laravel Sanctum для аутентикации, CSRF-защита включена по умолчанию для веб-маршрутов. Настроим Axios для отправки CSRF-токена с каждым запросом:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
// resources/js/utils/api.js
import axios from 'axios';
 
const api = axios.create({
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  },
  withCredentials: true // Важно для работы CSRF-защиты в SPA
});
 
export default api;
Убедитесь, что в вашем шаблоне app.blade.php есть мета-тег с CSRF-токеном:

HTML5
1
<meta name="csrf-token" content="{{ csrf_token() }}">

Защита от XSS-атак



Для защиты от XSS-атак в Vue.js:

1. Используйте v-bind для привязки URL:

TypeScript
1
2
3
4
5
<!-- Небезопасно -->
<a href="{{ user.website }}">Сайт пользователя</a>
 
<!-- Безопасно -->
<a :href="user.website">Сайт пользователя</a>
2. Избегайте использования v-html с ненадежными данными:

TypeScript
1
2
3
4
5
<!-- Небезопасно, если данные не проверены -->
<div v-html="userProvidedContent"></div>
 
<!-- Безопасно -->
<div>{{ userProvidedContent }}</div>
3. Для отображения HTML-контента с фильтрацией вредоносного кода можно использовать библиотеки, такие как DOMPurify:

Bash
1
npm install dompurify
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
  <div v-html="sanitizedContent"></div>
</template>
 
<script>
import DOMPurify from 'dompurify';
 
export default {
  props: {
    content: String
  },
  computed: {
    sanitizedContent() {
      return DOMPurify.sanitize(this.content);
    }
  }
}
</script>
На стороне Laravel также следует проводить валидацию входящих данных:

PHP
1
2
3
4
5
$validated = $request->validate([
    'name' => 'required|string|max:255',
    'description' => 'nullable|string',
    'website' => 'nullable|url',
]);

Безопасное хранение чувствительных данных на клиенте



Для хранения чувствительных данных в браузере:

1. Не используйте localStorage для хранения токенов аутентификации - предпочтительнее использовать HttpOnly куки.

2. При необходимости шифруйте данные перед сохранением. Например, с использованием Crypto API:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
async function encryptData(data, password) {
  const encoder = new TextEncoder();
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    encoder.encode(password),
    { name: 'PBKDF2' },
    false,
    ['deriveBits', 'deriveKey']
  );
  
  const key = await crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt,
      iterations: 100000,
      hash: 'SHA-256'
    },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
  
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    {
      name: 'AES-GCM',
      iv
    },
    key,
    encoder.encode(JSON.stringify(data))
  );
  
  return {
    salt: Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join(''),
    iv: Array.from(iv).map(b => b.toString(16).padStart(2, '0')).join(''),
    data: Array.from(new Uint8Array(encrypted)).map(b => b.toString(16).padStart(2, '0')).join('')
  };
}
Реализация такой системы безопасности может показаться сложной, но она необходима для защиты данных пользователей в современном веб-пространстве, где киберугрозы становятся все более изощренными.
Комплексный подход к безопасности, включающий защиту как на стороне сервера, так и на стороне клиента, позволит создать надёжное и защищенное веб-приложение на основе Laravel и Vue.js.

VUE CLI (CLI-VUE-BABEL) не собирается в работающий проект
После сборки проекта, сам проект не работает (RAGE MP сервер не открывает браузер). Проблемы в...

Обмен данными между Laravel и Vue.js
Пишем приложение, сначала все вьюшки были через обычные blade. Теперь начали переводить все на...

Настройка nginx для api laravel + vue js (file not found)
на проекте используется сборка https://github.com/codecasts/spa-starter-kit laravel лежит в...

Vue (в проекте Laravel) не показывает данные из mysql
я в Vue новичок пытаюсь в простом примере загрузить все данные из таблицы &quot;products&quot; при этом в...

Laravel + Vue непонятная ситуация
Здравствуйте, возникла необходимость изучить и начать пользовать фреймворком laravel, при малейшем...

Laravel + vue pusher система диалогов
Как решить проблемы: 1. Когда выбрал диалог - подключился к каналу для обмена сообщениями, все...

Использование нескольких экземпляров vue + laravel
Доброго времени суток. Возникла необходимость не писать весь код для вывода в id = app, а для...

Laravel SSR VUE как установить?
Есть пример https://github.com/anthonygore/vue-js-laravel-ssr Он разворачивается без проблем,...

Как взаимодействуют Laravel c Vue.js находящихся в двух отдельных папках?
Всем привет. Есть задача - сделать микросервисную архитектуру проекта, чтобы было две отдельные...

Пример использования микросервисной архитектуры в Vue js + laravel
Собираюсь делать проект с микросервисной архитектурой на vue js + laravel, хотелось бы увидеть...

Вывод медиа файлов в чате vue laravel
Как лучше организовать вывод изображений в чате? Чат делал на vue js таблица messages- id-...

PHP, Laravel, Vue, NuxtJS
Нужно писать web-приложение на Laravel и VueJS. NuxtJS - это, как я понил нода js. И к php его...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Генераторы Python для эффективной обработки данных
AI_Generated 21.05.2025
В Python существует инструмент настолько мощный и в то же время недооценённый, что я часто сравниваю его с тайным оружием в арсенале программиста. Речь идёт о генераторах — одной из самых элегантных. . .
Чем заменить Swagger в .NET WebAPI
stackOverflow 21.05.2025
Если вы создавали Web API на . NET в последние несколько лет, то наверняка сталкивались с зелёным интерфейсом Swagger UI. Этот инструмент стал практически стандартом для документирования и. . .
Использование Linq2Db в проектах C# .NET
UnmanagedCoder 21.05.2025
Среди множества претендентов на корону "идеального ORM" особое место занимает Linq2Db — микро-ORM, балансирующий между мощью полноценных инструментов и легковесностью ручного написания SQL. Что. . .
Реализация Domain-Driven Design с Java
Javaican 20.05.2025
DDD — это настоящий спасательный круг для проектов со сложной бизнес-логикой. Подход, предложенный Эриком Эвансом, позволяет создавать элегантные решения, которые точно отражают реальную предметную. . .
Возможности и нововведения C# 14
stackOverflow 20.05.2025
Выход версии C# 14, который ожидается вместе с . NET 10, приносит ряд интересных нововведений, действительно упрощающих жизнь разработчиков. Вы уже хотите опробовать эти новшества? Не проблема! Просто. . .
Собеседование по Node.js - вопросы и ответы
Reangularity 20.05.2025
Каждому разработчику рано или поздно приходится сталкиватся с техническими собеседованиями - этим стрессовым испытанием, где решается судьба карьерного роста и зарплатных ожиданий. В этой статье я. . .
Cython и C (СИ) расширения Python для максимальной производительности
py-thonny 20.05.2025
Python невероятно дружелюбен к начинающим и одновременно мощный для профи. Но стоит лишь заикнуться о высокопроизводительных вычислениях — и энтузиазм быстро улетучивается. Да, Питон медлительнее. . .
Безопасное программирование в Java и предотвращение уязвимостей (SQL-инъекции, XSS и др.)
Javaican 19.05.2025
Самые распространёные векторы атак на Java-приложения за последний год выглядят как классический "топ-3 хакерских фаворитов": SQL-инъекции (31%), межсайтовый скриптинг или XSS (28%) и CSRF-атаки. . .
Введение в Q# - язык квантовых вычислений от Microsoft
EggHead 19.05.2025
Microsoft вошла в гонку технологических гигантов с собственным языком программирования Q#, специально созданным для разработки квантовых алгоритмов. Но прежде чем погружаться в синтаксические дебри. . .
Безопасность Kubernetes с Falco и обнаружение вторжений
Mr. Docker 18.05.2025
Переход организаций к микросервисной архитектуре и контейнерным технологиям сопровождается лавинообразным ростом векторов атак — от тривиальных попыток взлома до многоступенчатых кибератак, способных. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru