Выбор правильного технологического стека определяет успех веб-проекта. 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 введите в терминале:
Если 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.
Чтобы проверить корректность установки, выполните команды:
После установки Node.js перейдите в корневой каталог вашего Laravel-проекта и установите необходимые JavaScript-зависимости:
Bash | 1
2
| cd my-project
npm install |
|
Интеграция Vue.js в Laravel-проект
Laravel имеет встроенную поддержку Vue.js через пакет Laravel Mix, который представляет собой обёртку над webpack. Сначала нужно установить Vue.js через NPM:
Затем откройте файл 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-стили.
Для сборки ресурсов выполните:
Если вы хотите, чтобы ресурсы автоматически пересобирались при изменении файлов, используйте:
Подготовка базы данных и настройка .env файла
Для работы с базой данных скопируйте файл .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 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 выполните:
Настройка маршрутизации для 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:
Создайте конфигурацию для 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 |
|
После установки запустите контейнеры:
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);
}
} |
|
Запустим миграции командой:
Разработка 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:
Создадим базовую структуру хранилища в 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();
} |
|
Для запуска обработчика очередей в рабочей среде используйте:
Оптимизация производительности
Оптимизация производительности является критически важным аспектом при разработке веб-приложений. В контексте связки Laravel и Vue.js можно применить несколько эффективных подходов. Для оптимизации фронтенда при сборке продакшн-версии приложения используйте:
Этот скрипт запускает 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:
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 новичок
пытаюсь в простом примере загрузить все данные из таблицы "products"
при этом в... 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 его...
|