Как создать компонент Vue в Laravel через Vite

Чтобы создать компонент Vue в Laravel, подключите Vue 3 и плагин Vite, смонтируйте компонент через createApp и передайте данные из Blade через props. Сквозной пример покажет счетчик, GET- и POST-запросы, CSRF-защиту, запуск HMR и production-сборку.

Что понадобится

Отправная точка – работающее приложение Laravel на Blade с настроенным Vite. Пример использует API Vue 3 и однофайловые компоненты.

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

Компонент окруженияУсловие совместимости
LaravelПроект поддерживает директиву Blade @vite и класс Illuminate\Support\Js. Маршруты в routes/web.php используют сессии и CSRF-защиту.
PHP и расширенияОкружение удовлетворяет требованиям пакетов из composer.lock. Команда composer check-platform-reqs завершается без ошибок требований платформы.
ComposerComposer читает lock-файл проекта и поддерживает API установленных Composer-плагинов. Команда composer install завершается без ошибок совместимости.
Node.js и npmNode.js соответствует полю engines выбранных пакетов Vite и его плагинов. Установка npm-зависимостей проходит без предупреждений EBADENGINE и конфликтов зависимостей.
Фронтенд-зависимостиПроект использует Vue 3. Пакеты vite, laravel-vite-plugin и @vitejs/plugin-vue удовлетворяют взаимным ограничениям peerDependencies.

Для существующего приложения сохраните совместимую связку Laravel, PHP и Composer. Подключение Vue само по себе не требует обновления серверного фреймворка. Если проект использует Mix, сначала подготовьте переход на Vite.

Все команды выполняйте из корня приложения. Перед изменениями сохраните проект в системе контроля версий, включая конфигурацию и lock-файлы.

В терминале из корня приложения:

php -v
composer --version
node -v
npm -v
composer install
composer check-platform-reqs
composer show laravel/framework
npm install
npm ls vite laravel-vite-plugin

Первые команды показывают версии окружения. Composer устанавливает зависимости и проверяет требования к PHP и расширениям. Команда composer show показывает установленный Laravel, а npm ls – пакеты сборщика.

Если npm сообщает о несовместимости, устраните ее до подключения Vue. Для EBADENGINE установите Node.js из диапазона engines, указанного в сообщении. При конфликте peerDependencies подберите версии пакетов под ограничения установленного сборщика.

Не обходите ограничения флагами --force или --legacy-peer-deps: установка не исправит несовместимость сборщика.

Для нового приложения сначала подготовьте обычный Laravel-проект на Blade с Vite. Для примера нужны маршрут из routes/web.php и рабочая папка resources. Если проект уже использует Inertia или другую точку входа, сохраните ее импорт и подключайте Vue внутри существующей архитектуры.

Установка Vue и плагина для Vite

Пакет Vue отвечает за интерфейс. Плагин @vitejs/plugin-vue обрабатывает однофайловые компоненты, а laravel-vite-plugin связывает Vite с точками входа Laravel.

Установите зависимости в корне проекта.

npm install vue@3
npm install --save-dev vite laravel-vite-plugin @vitejs/plugin-vue

Откройте файл vite.config.js. Сохраните уже существующие точки входа и плагины, если проект использует их для других ресурсов.

Файл vite.config.js:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
});

Параметр transformAssetUrls помогает Vite обработать ресурсы в шаблонах Vue. Эти опции приведены в документации Laravel по Vue и Vite.

Такой конфигурации достаточно, когда приложение импортирует однофайловые компоненты из JavaScript.

Сохраните файл и запустите сборщик после создания точки входа. Если проект содержит собственный vite.config.js, не заменяйте его целиком: добавьте плагин Vue в существующий список.

Создание компонента Vue

Создайте каталог resources/js/components и файл CounterCard.vue. Компонент будет хранить счетчик, принимать объект через prop и обращаться к двум маршрутам Laravel.

Файл resources/js/components/CounterCard.vue:

<script setup>
import { ref } from 'vue';

const props = defineProps({
    initialData: {
        type: Object,
        default: () => ({}),
    },
});

const count = ref(props.initialData.initialCount ?? 0);
const loading = ref(false);
const message = ref('');
const error = ref('');

async function requestServer(method) {
    loading.value = true;
    message.value = '';
    error.value = '';

    try {
        const options = {
            method,
            credentials: 'same-origin',
            headers: { Accept: 'application/json' },
        };
        let url = props.initialData.statusUrl;

        if (method === 'POST') {
            const token = document.querySelector(
                'meta[name="csrf-token"]'
            )?.content;

            if (!token) {
                throw new Error('На странице отсутствует CSRF-токен.');
            }

            url = props.initialData.saveUrl;
            options.headers['Content-Type'] = 'application/json';
            options.headers['X-CSRF-TOKEN'] = token;
            options.body = JSON.stringify({ count: count.value });
        }

        const response = await fetch(url, options);

        if (!response.ok) {
            throw new Error(`Сервер вернул HTTP ${response.status}.`);
        }

        const data = await response.json();
        message.value = data.message;
    } catch (exception) {
        error.value = exception instanceof Error
            ? exception.message
            : 'Не удалось выполнить запрос.';
    } finally {
        loading.value = false;
    }
}
</script>

<template>
    <section class="counter-card">
        <h2>{{ props.initialData.heading || 'Счетчик Vue' }}</h2>
        <p>Значение: {{ count }}</p>
        <button type="button" :disabled="loading" @click="count++">
            Увеличить
        </button>
        <button type="button" :disabled="loading" @click="requestServer('GET')">
            Получить ответ
        </button>
        <button type="button" :disabled="loading" @click="requestServer('POST')">
            Отправить счетчик
        </button>
        <ul>
            <li v-for="item in props.initialData.items || []" :key="item.id">
                {{ item.title }}
            </li>
        </ul>
        <p v-if="loading" role="status">Загрузка...</p>
        <p v-if="message" role="status">{{ message }}</p>
        <p v-if="error" role="alert">{{ error }}</p>
    </section>
</template>

<style scoped>
.counter-card {
    max-width: 36rem;
    padding: 1.5rem;
    border: 1px solid #ccc;
    border-radius: 0.75rem;
}

button {
    margin-right: 0.5rem;
    padding: 0.5rem 0.75rem;
}
</style>

Синтаксис script setup объявляет props и функции прямо внутри компонента. Функция ref создает реактивное значение: в JavaScript оно доступно через count.value, а в шаблоне Vue раскрывает его как count.

Кнопка меняет локальное состояние без запроса.

Две другие кнопки используют URL из props, поэтому компонент не хранит адреса контроллера в нескольких местах.

Подключение компонента в app.js

Точка входа импортирует компонент, создает приложение и монтирует его в элемент #app. Объект из глобальной переменной станет prop корневого компонента.

Файл resources/js/app.js:

import { createApp } from 'vue';
import CounterCard from './components/CounterCard.vue';

const root = document.querySelector('#app');

if (root) {
    createApp(CounterCard, {
        initialData: window.vueExampleData ?? {},
    }).mount(root);
}

Такой способ сразу монтирует CounterCard и не требует регистрации тега. После загрузки страницы Vue заменит содержимое корневого элемента разметкой компонента.

Как монтировать несколько независимых компонентов

Создайте отдельный экземпляр Vue для каждого контейнера. Каждый экземпляр получит собственный prop и собственное состояние.

Файл resources/js/app.js для нескольких виджетов:

import { createApp } from 'vue';
import CounterCard from './components/CounterCard.vue';

const widgets = window.vueExampleWidgets ?? {};

for (const element of document.querySelectorAll('[data-vue-counter]')) {
    const key = element.dataset.vueCounter;

    createApp(CounterCard, {
        initialData: widgets[key] ?? {},
    }).mount(element);
}

На странице разместите несколько элементов с разными значениями data-vue-counter. Объект window.vueExampleWidgets должен содержать данные по этим ключам.

Не запускайте оба варианта на одной странице: один корень не должен получать два приложения.

Вывод компонента в шаблоне Blade

Layout подключает CSS и JavaScript через @vite. Корневой элемент оставьте пустым, потому что импортированный компонент сам создаст разметку.

Файл resources/views/layouts/app.blade.php:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Компонент Vue в Laravel</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    @yield('content')
</body>
</html>

Файл resources/views/vue-example.blade.php:

@extends('layouts.app')

@section('content')
    <div id="app"></div>

    <script>
        window.vueExampleData = {{ Illuminate\Support\Js::from($initialData) }};
    </script>
@endsection

Скрипт задает данные до запуска модуля Vite. После загрузки страницы в #app появится карточка со счетчиком.

Добавьте маршруты страницы и запросов к серверу.

Файл routes/web.php:

<?php

use App\Http\Controllers\VueExampleController;
use Illuminate\Support\Facades\Route;

Route::get('/vue-example', [VueExampleController::class, 'show'])
    ->name('vue-example.show');

Route::get('/vue-example/status', [VueExampleController::class, 'status'])
    ->name('vue-example.status');

Route::post('/vue-example/save', [VueExampleController::class, 'save'])
    ->name('vue-example.save');

В существующем файле сохраните остальные маршруты и не дублируйте импорты. Для открытия страницы также понадобится контроллер VueExampleController.

Передача данных из Laravel в компонент

Контроллер подготовит объект, а Blade безопасно передаст его в JavaScript. В prop отправляйте только поля, которые разрешено показывать посетителю.

Файл app/Http/Controllers/VueExampleController.php:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class VueExampleController extends Controller
{
    public function show()
    {
        $initialData = [
            'heading' => 'Счетчик из контроллера',
            'initialCount' => 2,
            'items' => [
                ['id' => 1, 'title' => 'Данные переданы через props'],
                ['id' => 2, 'title' => 'Кнопки обрабатывает Vue'],
            ],
            'statusUrl' => route('vue-example.status', [], false),
            'saveUrl' => route('vue-example.save', [], false),
        ];

        return view('vue-example', compact('initialData'));
    }

    public function status()
    {
        return response()->json([
            'message' => 'Laravel ответил на GET-запрос.',
        ]);
    }

    public function save(Request $request)
    {
        $validated = $request->validate([
            'count' => ['required', 'integer', 'min:0', 'max:1000'],
        ]);

        return response()->json([
            'message' => 'Сервер получил значение: ' . $validated['count'],
            'count' => (int) $validated['count'],
        ]);
    }
}

Js::from формирует JavaScript-выражение с экранированием для HTML-контекста. Не оборачивайте результат в JSON.parse: выражение уже создает объект.

Используйте показанный вызов Js::from($initialData), чтобы передать данные из Laravel во Vue. Он подходит для вложенных массивов и объектов.

Секреты, токены доступа и закрытые поля в prop не передавайте.

Когда нужен компилятор шаблонов Vue

При вызове createApp(CounterCard, { initialData }).mount(element) шаблон уже обработал Vite. Для этого варианта пустого корневого контейнера достаточно, поэтому alias не нужен.

Если Blade содержит тег компонента внутри #app, Vue должен разобрать этот DOM-шаблон. Для такого способа нужна сборка Vue с компилятором. Разницу между сборками объясняет документация Vue по инструментам разработки.

Чтобы выводить компонент через тег, замените три файла следующими вариантами. Контроллер, маршруты, layout и сам CounterCard.vue остаются прежними.

Файл vite.config.js для варианта с тегом компонента:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    resolve: {
        alias: {
            vue: 'vue/dist/vue.esm-bundler.js',
        },
    },
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
});

Файл resources/js/app.js для варианта с тегом компонента:

import { createApp } from 'vue';
import CounterCard from './components/CounterCard.vue';

const root = document.querySelector('#app');

if (root) {
    const app = createApp({
        data() {
            return {
                initialData: window.vueExampleData ?? {},
            };
        },
    });

    app.component('counter-card', CounterCard);
    app.mount('#app');
}

Файл resources/views/vue-example.blade.php для варианта с тегом компонента:

@extends('layouts.app')

@section('content')
    <div id="app">
        <counter-card :initial-data="initialData"></counter-card>
    </div>

    <script>
        window.vueExampleData = {{ Illuminate\Support\Js::from($initialData) }};
    </script>
@endsection

Метод app.component регистрирует тег counter-card внутри этого приложения. Привязка :initial-data передает объект из корневого состояния в prop компонента.

Перезапустите Vite после изменения конфигурации и перезагрузите страницу. Результат останется прежним: карточка покажет заголовок и начальное значение 2. Для одиночного виджета прямое монтирование проще: оно обходится без компилятора в браузере.

Запрос к серверу из компонента

Компонент отправляет GET для статуса и POST со значением счетчика. Контроллер возвращает JSON, а компонент показывает состояние загрузки, сообщение или ошибку.

Маршруты связывают URL с методами status и save. GET-маршрут возвращает сообщение без входных данных. POST-маршрут принимает поле count и проверяет его до формирования ответа.

Для POST компонент читает CSRF-токен из meta-тега и передает его в заголовке X-CSRF-TOKEN. Параметр credentials: 'same-origin' отправляет cookies текущего сайта вместе с запросом.

После нажатия «Получить ответ» сервер вернет JSON со статусом. Кнопка «Отправить счетчик» отправит текущее число и покажет сообщение контроллера.

Этот пример не сохраняет счетчик в базе данных.

Ответ fetch нужно проверять через response.ok, потому что HTTP-ошибка сама по себе не попадает в catch. Описание Fetch API есть в документации MDN.

Запуск и сборка

В разработке Laravel и Vite работают одновременно. Первый процесс отдает страницу, второй обслуживает модули Vue и обновляет их через HMR.

В двух терминалах из корня приложения:

php artisan serve
npm run dev

Запустите команды по одной в каждом терминале. Возьмите адрес сервера из вывода php artisan serve и добавьте к нему путь /vue-example. Откройте полученный адрес в браузере.

Вы увидите заголовок «Счетчик из контроллера», значение 2 и три кнопки. Нажатие «Увеличить» меняет число без перезагрузки страницы.

После изменения CounterCard.vue браузер обновит модуль без ручной сборки. Нажмите кнопки запросов и откройте Network в инструментах разработчика. GET и POST должны вернуть JSON, а карточка – показать сообщение сервера.

Для production-сборки остановите Vite сочетанием Ctrl+C. Дождитесь завершения процесса и возвращения приглашения командной строки.

В терминале из корня приложения:

npm run build

Команда подготовит CSS и JavaScript для production. Откройте страницу с остановленным Vite: браузер должен загрузить готовую сборку. В Network убедитесь, что ресурсы загружаются с сервера приложения. Счетчик и обе кнопки запросов должны работать без dev-сервера.

Для просмотра состояния компонента используйте Vue Devtools в режиме разработки. Откройте официальную документацию Vue по расширению и перейдите по ссылке Vue Devtools на страницу установки.

Установите расширение для вашего браузера, запустите npm run dev и перезагрузите страницу. Откройте инструменты разработчика и панель расширения Vue. В дереве компонентов найдите CounterCard, затем посмотрите prop initialData и реактивное значение count.

Чтобы отменить подключение, восстановите измененные файлы и lock-файлы из сохраненной версии проекта. После восстановления выполните npm install и заново соберите ресурсы.

Ошибки при подключении Vue к Laravel

Симптом показывает, на каком участке оборвалась цепочка: загрузка entry-файла, монтаж, связь с Vite или ответ Laravel.

СимптомПричинаИсправление
Пустой #appEntry-файл не загрузился или корневой элемент отсутствуетПосмотрите первую ошибку Console и наличие resources/js/app.js в Network. Сверьте #app в Blade и JavaScript.
Runtime-only buildVue получил DOM-шаблон без компилятораМонтируйте импортированный CounterCard напрямую в пустой контейнер, как в примере. Для DOM-шаблонов нужен alias vue/dist/vue.esm-bundler.js.
Vite manifest not foundНет dev-сервера и production-сборкиДля разработки запустите npm run dev, для production выполните npm run build.
Vite недоступен в DockerПорт контейнера не опубликован или HMR использует недоступное имяОпубликуйте порт Vite и задайте host, который доступен браузеру.
Стили или изображения не загружаютсяПуть не совпадает с точкой входа или местом храненияСопоставьте путь в @vite с input. Создайте resources/css/app.css, если файла нет; для примера он может быть пустым. Ресурсы из public вызывайте от корня сайта, ресурсы из resources импортируйте через Vite.
POST возвращает 419CSRF-токен или session cookie не отправленыПроверьте meta-тег, заголовок X-CSRF-TOKEN и cookies в Network.
POST возвращает 422Данные не прошли валидациюПередайте целое число от 0 до 1000, как требует контроллер.

Для Docker или Sail добавьте сетевые параметры в объект defineConfig файла vite.config.js, рядом с plugins. Этот фрагмент задает порт примера, который также нужно опубликовать в конфигурации контейнера.

Дополнение к vite.config.js внутри объекта конфигурации:

server: {
    host: '0.0.0.0',
    port: 5173,
    strictPort: true,
    hmr: {
        host: 'localhost',
    },
},

Если браузер работает на другой машине, замените localhost на имя, доступное этой машине. После изменения конфигурации перезапустите Vite.

Альтернативы: стартовый набор с Vue и Inertia

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

Inertia удобнее для приложения, где Vue управляет целыми страницами. Контроллер передает props странице Vue, поэтому глобальные переменные и отдельные корни обычно не нужны.

Выберите официальный стартовый набор Laravel с Vue и Inertia, если основную часть интерфейса планируете писать на Vue.

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

Если проект на Laravel Mix и Vue 2

Файлы webpack.mix.js и зависимость laravel-mix указывают на старую схему сборки. Команды берутся из раздела scripts файла package.json, поэтому название npm run dev само по себе не определяет сборщик.

В терминале из корня Mix-проекта, если такие scripts заданы:

npm run dev
npm run watch

Команда watch следит за изменениями в проектах, где ее добавили в scripts. Она не переводит приложение на Vite автоматически.

Официальная поддержка Vue 2 завершилась.

Для нового кода выбирайте Vue 3 и переносите зависимости, конфигурацию, импорты и подключение ресурсов отдельными изменениями. Порядок перехода описывает руководство Laravel по миграции с Mix на Vite.

Не смешивайте new Vue и createApp в одной точке входа. Для Blade-виджета оставьте один корневой экземпляр, а для приложения со страницами рассмотрите Inertia.

Какой способ подключения выбрать

Для одного или нескольких интерактивных блоков на Blade достаточно Vite, createApp и однофайловых компонентов. Такой путь дает props из Laravel, запросы fetch и привычную серверную маршрутизацию.

Если Vue должен управлять страницами целиком, берите связку Laravel и Inertia. Она уменьшает ручной код между контроллером, Blade и корнем приложения.

Совет от нашей редакции: сначала соберите один компонент через пустой #app, затем добавляйте props и запросы. Так ошибка сборки не смешается с ошибкой маршрута или CSRF.

Комментарии

Оставьте свой комментарий
Пока никто не оставил комментариев