Inertia.js
Com construir SPAs amb Inertia.js i Laravel: renderitzar pàgines, formularis, navegació, dades compartides i Server-Side Rendering.
Que es Inertia.js?#
Quan desenvolupem aplicacions web modernes, sovint ens trobem davant d'una decisió fonamental d'arquitectura. Per una banda, podem construir una aplicació monolítica tradicional amb Laravel i Blade, on cada navegació provoca una recàrrega completa de la pàgina. Funciona bé, però l'experiència d'usuari no és tan fluida com la d'una Single Page Application (SPA). Per l'altra banda, podem construir una SPA completa amb Vue, React o Svelte, però això implica crear una API REST o GraphQL al servidor, gestionar l'autenticació amb tokens, duplicar la lògica de validació i routing, i mantenir dos projectes separats que han d'evolucionar en paral·lel.
Inertia.js neix precisament per resoldre aquest dilema. Es tracta d'una capa d'unió entre el teu framework de servidor (Laravel) i el teu framework de client (Vue, React o Svelte) que elimina la necessitat de construir una API. Amb Inertia, els teus controladors de Laravel no retornen vistes Blade ni respostes JSON: retornen respostes Inertia que renderitzen components de Vue o React directament.
El funcionament intern és elegant en la seva simplicitat. La primera vegada que un usuari visita la teva aplicació, Inertia serveix una pàgina HTML completa, exactament com ho faria una aplicació tradicional. Aquesta pàgina conté el component JavaScript necessari i les seves dades inicials. A partir d'aquí, totes les navegacions posteriors es converteixen en peticions XHR: quan l'usuari fa clic en un enllaç, Inertia intercepta el clic, fa una petició AJAX al servidor, rep les dades del nou component com a JSON, i actualitza el DOM sense recarregar la pàgina. L'usuari experimenta transicions suaus, idèntiques a les d'una SPA, però el desenvolupador treballa amb controladors i rutes de Laravel de tota la vida.
Es important entendre que Inertia no és un framework. No reemplaça ni Laravel ni Vue: és la cola que els uneix. No tens un router al client perquè fas servir el router de Laravel. No tens un sistema d'estat global perquè les dades arriben com a props des del controlador. No tens una API perquè el controlador parla directament amb el component. Aquesta filosofia s'anomena sovint "el monòlit clàssic amb interfície moderna", i és la raó per la qual Inertia s'ha convertit en una opció tan popular dins l'ecosistema Laravel.
Instal·lació#
La instal·lació d'Inertia requereix configurar tant el costat servidor (Laravel) com el costat client (Vue, React o Svelte). Comencem pel servidor.
Costat servidor#
Primer, instal·la el paquet d'Inertia per a Laravel amb Composer:
composer require inertiajs/inertia-laravelTot seguit, necessites una plantilla arrel de Blade que serveixi com a contenidor per a la teva aplicació. Crea o edita el fitxer resources/views/app.blade.php:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
@vite('resources/js/app.js')
@inertiaHead
</head>
<body>
@inertia
</body>
</html>La directiva @inertia crea un element <div> amb un atribut data-page que conté les dades de la pàgina inicial codificades en JSON. Quan l'aplicació JavaScript s'inicialitza, llegeix aquestes dades i renderitza el component corresponent. La directiva @inertiaHead permet que Inertia gestioni el contingut del <head>, com ara el títol de la pàgina.
Ara necessites configurar el middleware d'Inertia. Executa la comanda Artisan per generar-lo:
php artisan inertia:middlewareAixò crea el fitxer app/Http/Middleware/HandleInertiaRequests.php. Registra'l al fitxer bootstrap/app.php dins del grup de middleware web:
// bootstrap/app.php
use App\Http\Middleware\HandleInertiaRequests;
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
HandleInertiaRequests::class,
]);
})Aquest middleware s'encarrega de detectar si una petició és una petició Inertia (les peticions XHR posteriors a la primera càrrega) i de gestionar el versionat d'assets, les dades compartides i la lògica de resposta adequada.
Costat client#
Ara configurem el frontend. En aquest exemple farem servir Vue 3, però el procés és molt similar per a React o Svelte. Instal·la les dependències necessàries:
npm install @inertiajs/vue3 vueSi prefereixes React, la instal·lació seria:
npm install @inertiajs/react react react-domConfigura el fitxer d'entrada de l'aplicació JavaScript. Amb Vue 3, el fitxer resources/js/app.js tindrà aquest aspecte:
// resources/js/app.js
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
createInertiaApp({
resolve: (name) => resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob('./Pages/**/*.vue')
),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el);
},
});La funció resolvePageComponent s'encarrega de trobar automàticament el component Vue que correspon a cada pàgina. Quan el controlador de Laravel retorna Inertia::render('Articles/Index'), Inertia buscarà el fitxer resources/js/Pages/Articles/Index.vue. El mètode import.meta.glob de Vite permet fer aquesta resolució dinàmica de forma eficient.
Finalment, configura Vite per treballar amb Vue. Al fitxer 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/js/app.js',
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
});La configuració transformAssetUrls és important perquè permet que les URL d'assets dins de les plantilles de Vue es resolguin correctament a través de Vite, en lloc d'intentar cercar-les com a fitxers locals. Amb això, la instal·lació està completa i pots començar a construir les teves pàgines.
Renderitzar pàgines#
La forma de renderitzar pàgines amb Inertia és molt similar a com ho fas amb Blade. En lloc de retornar una vista, retornes una resposta Inertia des del controlador. Això fa que la transició des d'una aplicació Blade tradicional sigui molt natural.
Considera un controlador típic per gestionar articles:
use Inertia\Inertia;
class ArticleController extends Controller
{
public function index()
{
return Inertia::render('Articles/Index', [
'articles' => Article::with('author')
->published()
->latest()
->paginate(15),
]);
}
public function show(Article $article)
{
return Inertia::render('Articles/Show', [
'article' => $article->load('author', 'comments.user'),
]);
}
public function create()
{
return Inertia::render('Articles/Create', [
'categories' => Category::all(),
]);
}
}Fixa't que els controladors són gairebé idèntics als que escriuries per a una aplicació Blade. La diferència principal és que en lloc de return view('articles.index', [...]) escrius return Inertia::render('Articles/Index', [...]). El primer argument és el nom del component (que correspon a la ruta dins de resources/js/Pages/), i el segon argument és un array de props que rebra el component.
Resolució de components#
Inertia utilitza una convenció basada en directoris per trobar els components de pàgina. Quan el controlador retorna Inertia::render('Articles/Index'), Inertia busca el component a resources/js/Pages/Articles/Index.vue. Aquesta estructura de carpetes et permet organitzar les pàgines de manera lògica, replicant la mateixa organització que tindries amb les vistes Blade.
Una estructura típica de pàgines seria:
resources/js/Pages/
Articles/
Index.vue
Show.vue
Create.vue
Edit.vue
Users/
Index.vue
Profile.vue
Dashboard.vue
Auth/
Login.vue
Register.vue
El component Vue corresponent rebrà les props que has passat des del controlador. Per exemple, el component Articles/Index.vue:
<!-- resources/js/Pages/Articles/Index.vue -->
<script setup>
defineProps({
articles: Object,
});
</script>
<template>
<div>
<h1>Articles</h1>
<article v-for="article in articles.data" :key="article.id">
<h2>{{ article.title }}</h2>
<p>Per {{ article.author.name }}</p>
<p>{{ article.excerpt }}</p>
</article>
</div>
</template>Controlar les dades que passes#
Un aspecte crític quan treballes amb Inertia és controlar quines dades envia el servidor al client. A diferència de Blade, on les dades es processen al servidor i mai surten d'allà, amb Inertia les dades es serialitzen a JSON i es transmeten al navegador. Això vol dir que qualsevol dada que passis com a prop serà visible a les eines de desenvolupador del navegador.
Per evitar enviar informació sensible o innecessària, utilitza API Resources d'Eloquent per transformar els models abans de passar-los:
use App\Http\Resources\ArticleResource;
public function index()
{
return Inertia::render('Articles/Index', [
'articles' => ArticleResource::collection(
Article::with('author')->published()->latest()->paginate(15)
),
]);
}Una alternativa més senzilla per a casos simples és seleccionar només les columnes que necessites:
public function index()
{
return Inertia::render('Articles/Index', [
'articles' => Article::query()
->select('id', 'title', 'excerpt', 'published_at')
->with('author:id,name')
->published()
->latest()
->paginate(15),
]);
}Props i dades#
Les props són el mecanisme principal pel qual les dades flueixen des del servidor cap als components de la interfície. Cada vegada que Inertia visita una pàgina, les props es passen al component com a propietats reactives. Quan l'usuari navega a una altra pàgina i després torna enrere, Inertia pot tornar a demanar les props actualitzades al servidor, garantint que la interfície sempre mostra dades fresques.
Props reactives#
Les props que rep un component d'Inertia són reactives per defecte. Quan Inertia fa una nova visita a la mateixa pàgina (per exemple, després d'enviar un formulari o fer un reload parcial), les props s'actualitzen automàticament i el component es re-renderitza amb les noves dades:
<script setup>
const props = defineProps({
users: Array,
filters: Object,
});
// Les props canvien automàticament quan Inertia torna a visitar la pàgina
</script>Lazy props#
No totes les dades d'una pàgina són necessàries immediatament. Imagina una pàgina de dashboard amb múltiples seccions: estadístiques generals, últims articles, activitat recent i gràfics. Carregar totes aquestes dades en cada visita seria ineficient. Les lazy props et permeten definir dades que només es carregaran quan es sol·licitin explícitament:
use Inertia\Inertia;
public function dashboard()
{
return Inertia::render('Dashboard', [
'stats' => fn () => Stats::calculate(),
'recentArticles' => Inertia::lazy(fn () =>
Article::latest()->take(5)->get()
),
'activityLog' => Inertia::lazy(fn () =>
Activity::latest()->take(20)->get()
),
]);
}La diferència entre un closure normal (fn () =>) i Inertia::lazy() és subtil pero important. Un closure normal s'avalua en cada visita completa a la pàgina, pero no en recarregues parcials tret que es demani explícitament. Una lazy prop, en canvi, mai es carrega en la visita inicial: només s'obté quan el client la sol·licita de forma explícita amb una recàrrega parcial.
Al component, pots demanar les lazy props quan les necessitis:
<script setup>
import { router } from '@inertiajs/vue3';
function loadActivity() {
router.reload({ only: ['activityLog'] });
}
</script>
<template>
<button @click="loadActivity">Carregar activitat</button>
</template>Deferred props#
Les deferred props són una variació molt útil de les lazy props. Quan defineixes una prop com a deferred, Inertia la carrega automàticament en una petició separada just després de la visita inicial a la pàgina. Això permet que la pàgina es renderitzi ràpidament amb les dades essencials, mentre les dades secundàries es carreguen en segon pla:
public function dashboard()
{
return Inertia::render('Dashboard', [
'stats' => $this->getBasicStats(),
'heavyReport' => Inertia::defer(fn () =>
Report::generateMonthly()
),
'charts' => Inertia::defer(fn () =>
ChartData::forDashboard()
),
]);
}Al component, les deferred props arriben com a undefined inicialment i es poblen automàticament quan la petició en segon pla es completa. Pots mostrar un estat de càrrega mentrestant:
<script setup>
defineProps({
stats: Object,
heavyReport: Object,
charts: Object,
});
</script>
<template>
<div>
<StatsPanel :stats="stats" />
<div v-if="heavyReport">
<ReportTable :data="heavyReport" />
</div>
<div v-else>
<LoadingSpinner />
</div>
</div>
</template>Merging props per a scroll infinit#
Quan implementes patrons com l'scroll infinit, necessites que les noves dades s'afegeixin a les existents en lloc de reemplaçar-les. Per defecte, Inertia substitueix completament cada prop en cada visita. El mètode merge canvia aquest comportament:
public function index(Request $request)
{
return Inertia::render('Articles/Index', [
'articles' => Inertia::merge(
Article::published()
->latest()
->paginate(15, page: $request->integer('page', 1))
->getCollection()
),
'page' => $request->integer('page', 1),
]);
}Al component, cada vegada que demanis la pàgina següent, els nous articles s'afegiran als existents en lloc de reemplaçar-los:
<script setup>
import { router } from '@inertiajs/vue3';
const props = defineProps({
articles: Array,
page: Number,
});
function loadMore() {
router.reload({
data: { page: props.page + 1 },
only: ['articles', 'page'],
});
}
</script>
<template>
<div>
<article v-for="article in articles" :key="article.id">
<h2>{{ article.title }}</h2>
</article>
<button @click="loadMore">Carregar mes</button>
</div>
</template>Dades compartides#
Sovint hi ha dades que necessites a totes les pàgines de la teva aplicació: l'usuari autenticat, missatges flash, el token CSRF, el nom de l'aplicació, etc. En lloc de repetir-les a cada controlador, Inertia et permet definir dades compartides que estaran disponibles automàticament a tots els components de pàgina.
Configurar dades compartides#
El lloc natural per definir les dades compartides és el middleware HandleInertiaRequests, concretament dins del mètode share. Inertia crida aquest mètode per a cada petició i combina el resultat amb les props de la pàgina:
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user()
? $request->user()->only('id', 'name', 'email', 'avatar')
: null,
],
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
'appName' => config('app.name'),
];
}Observa que les dades flash s'encapsulen dins d'un closure. Això no es fa per casualitat: els closures s'avaluen de manera tardana, cosa que significa que la sessió ja estarà disponible quan s'executin. Si passessis $request->session()->get('success') directament sense closure, podria no funcionar correctament en determinades circumstàncies.
El mètode parent::share($request) inclou les dades compartides per defecte d'Inertia, com ara els errors de validació. Es important mantenir-lo per no perdre aquesta funcionalitat integrada.
Accedir a les dades compartides als components#
Les dades compartides estan disponibles a tots els components de pàgina a través del helper usePage:
<script setup>
import { usePage, Link } from '@inertiajs/vue3';
import { computed } from 'vue';
const page = usePage();
const user = computed(() => page.props.auth.user);
const flash = computed(() => page.props.flash);
</script>
<template>
<div>
<nav v-if="user">
<span>Hola, {{ user.name }}</span>
<Link href="/logout" method="post" as="button">
Tancar sessio
</Link>
</nav>
<nav v-else>
<Link href="/login">Iniciar sessio</Link>
</nav>
<div v-if="flash.success" class="alert alert-success">
{{ flash.success }}
</div>
<div v-if="flash.error" class="alert alert-danger">
{{ flash.error }}
</div>
</div>
</template>Aquesta tècnica és especialment potent per construir layouts persistents que mostren informació de l'usuari, notificacions i missatges flash sense haver de passar aquestes dades des de cada controlador individualment.
Links i navegació#
La navegació és on Inertia realment brilla. En lloc d'usar etiquetes <a> normals que provoquen recàrregues completes de pàgina, Inertia proporciona un component Link que intercepta els clics i fa les transicions de pàgina via XHR, mantenint l'experiència de SPA.
El component Link#
El component Link d'Inertia reemplaça les etiquetes <a> tradicionals. Quan l'usuari fa clic en un Link, Inertia intercepta l'event, fa una petició XHR al servidor, rep les dades del nou component, i actualitza el DOM sense recarregar la pàgina. L'URL del navegador s'actualitza correctament i l'historial de navegació funciona com s'espera:
<script setup>
import { Link } from '@inertiajs/vue3';
</script>
<template>
<nav>
<Link href="/">Inici</Link>
<Link href="/articles">Articles</Link>
<Link href="/about">Sobre nosaltres</Link>
</nav>
</template>El component Link genera una etiqueta <a> al DOM, de manera que és completament accessible i compatible amb els estàndards web. Els usuaris poden fer clic dret per obrir en una nova pestanya, i els motors de cerca poden seguir els enllaços normalment.
Mètodes HTTP als links#
Per defecte, Link fa peticions GET. Pero pots especificar altres mètodes HTTP, cosa que és molt útil per a accions com tancar sessió o eliminar un recurs:
<template>
<Link href="/logout" method="post" as="button">
Tancar sessio
</Link>
<Link
:href="`/articles/${article.id}`"
method="delete"
as="button"
:onBefore="() => confirm('Segur que vols eliminar?')"
>
Eliminar article
</Link>
</template>L'atribut as controla quin element HTML es renderitza. Quan fas servir mètodes com POST o DELETE, es recomanable renderitzar un <button> en lloc d'un <a> per raons d'accessibilitat i semàntica.
Preservar estat i scroll#
Quan navegues entre pàgines, Inertia reseteja l'estat del component i la posició del scroll per defecte. De vegades vols mantenir l'estat actual, per exemple quan apliques filtres a una taula i vols que la selecció es mantingui:
<template>
<Link href="/articles?sort=date" preserve-state>
Ordenar per data
</Link>
<Link href="/articles?page=2" preserve-scroll>
Pagina segent
</Link>
<Link href="/articles?category=tech" preserve-state preserve-scroll>
Filtrar per tecnologia
</Link>
</template>L'atribut preserve-state manté l'estat local del component (per exemple, dades de formularis no enviats). L'atribut preserve-scroll manté la posició del scroll al mateix punt en lloc de tornar al principi de la pàgina. Ambdos es poden combinar segons les necessitats.
L'atribut replace fa que la navegació reemplaci l'entrada actual a l'historial del navegador en lloc d'afegir-ne una de nova. Això es útil per a filtres o ordenaments on no vols que l'usuari hagi de fer clic enrere múltiples vegades per tornar a la pàgina anterior:
<template>
<Link href="/articles?sort=date" replace>
Ordenar per data
</Link>
</template>Navegació programàtica#
No sempre naveguem amb clics de l'usuari. De vegades necessites navegar des del codi, per exemple, després de completar una acció o en resposta a un event. El router d'Inertia proporciona mètodes per fer-ho:
<script setup>
import { router } from '@inertiajs/vue3';
function goToArticle(id) {
router.visit(`/articles/${id}`);
}
function deleteArticle(id) {
if (confirm('Segur que vols eliminar?')) {
router.delete(`/articles/${id}`, {
onSuccess: () => {
// Redirecció o notificació
},
});
}
}
function applyFilters(filters) {
router.get('/articles', filters, {
preserveState: true,
preserveScroll: true,
});
}
function submitData(data) {
router.post('/articles', data, {
onSuccess: () => console.log('Creat!'),
onError: (errors) => console.log('Errors:', errors),
onFinish: () => console.log('Peticio completada'),
});
}
</script>Els mètodes disponibles són router.visit(), router.get(), router.post(), router.put(), router.patch() i router.delete(). Cadascun accepta una URL, dades opcionals i un objecte d'opcions que pot incloure callbacks per gestionar el cicle de vida de la petició.
Indicador de progrés#
Inertia inclou un indicador de progrés integrat que es mostra durant les navegacions. Per defecte, apareix com una barra fina a la part superior de la pàgina. Pots configurar-lo durant la inicialització de l'aplicació:
createInertiaApp({
progress: {
delay: 250,
color: '#4B5563',
includeCSS: true,
showSpinner: false,
},
// ...
});El paràmetre delay estableix quants mil·lisegons esperar abans de mostrar l'indicador, evitant que aparegui en navegacions ràpides. El color i l'aparença es poden personalitzar segons el disseny de la teva aplicació. Si prefereixes gestionar el progrés tu mateix, pots desactivar-lo amb progress: false i escoltar els events del router.
Enllaços externs#
Quan necessites navegar a una URL externa o a una pàgina que no està gestionada per Inertia, simplement utilitza una etiqueta <a> normal. Inertia no interfereix amb els enllaços normals: nomes intercepta els components Link:
<template>
<a href="https://laravel.com">Visitar Laravel.com</a>
<a href="/descarregar-pdf">Descarregar PDF</a>
</template>Formularis#
La gestió de formularis és un dels punts forts d'Inertia. El helper useForm simplifica enormement el procés de crear, enviar i gestionar l'estat dels formularis, incloent-hi la validació del servidor, l'estat de càrrega i el reset de dades.
El helper useForm#
El helper useForm crea un objecte reactiu que gestiona tot l'estat del formulari. Inclou les dades del formulari, els errors de validació, l'estat de processament i mètodes per enviar, resetejar i transformar les dades:
<script setup>
import { useForm } from '@inertiajs/vue3';
const form = useForm({
title: '',
body: '',
category_id: null,
published: false,
});
function submit() {
form.post('/articles', {
preserveScroll: true,
onSuccess: () => form.reset(),
});
}
</script>
<template>
<form @submit.prevent="submit">
<div>
<label for="title">Titol</label>
<input
id="title"
v-model="form.title"
type="text"
/>
<p v-if="form.errors.title" class="text-red-600">
{{ form.errors.title }}
</p>
</div>
<div>
<label for="body">Contingut</label>
<textarea
id="body"
v-model="form.body"
></textarea>
<p v-if="form.errors.body" class="text-red-600">
{{ form.errors.body }}
</p>
</div>
<div>
<label>
<input
v-model="form.published"
type="checkbox"
/>
Publicar immediatament
</label>
</div>
<button type="submit" :disabled="form.processing">
{{ form.processing ? 'Enviant...' : 'Crear article' }}
</button>
</form>
</template>Estat de processament i errors#
Quan envies un formulari amb useForm, l'objecte form gestiona automàticament l'estat de la petició. La propietat form.processing es posa a true mentre la petició està en curs, cosa que et permet desactivar el botó d'enviament per evitar enviaments duplicats. Quan el servidor retorna errors de validació (una resposta 422), Inertia els assigna automàticament a form.errors, un objecte on cada clau correspon al camp amb error.
Això funciona gràcies a la integració entre Inertia i la validació de Laravel. Al controlador, simplement valides com sempre:
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
'category_id' => 'nullable|exists:categories,id',
'published' => 'boolean',
]);
$article = Article::create($validated);
return redirect()
->route('articles.show', $article)
->with('success', 'Article creat correctament.');
}Si la validació falla, Laravel retorna automàticament una resposta 422 amb els errors, i Inertia els recull i els assigna a form.errors. Si la validació passa, el controlador fa una redirecció que Inertia segueix automàticament, navegant a la nova pàgina sense recàrrega.
Reset i neteja d'errors#
L'objecte form proporciona mètodes per resetejar les dades i netejar els errors de validació. Això es útil quan vols reutilitzar el formulari o netejar errors específics:
<script setup>
import { useForm } from '@inertiajs/vue3';
const form = useForm({
name: '',
email: '',
role: 'user',
});
function submit() {
form.post('/users');
}
function resetForm() {
form.reset();
}
function resetEmail() {
form.reset('email');
}
function clearEmailError() {
form.clearErrors('email');
}
function clearAllErrors() {
form.clearErrors();
}
</script>El mètode form.reset() sense arguments restaura totes les dades als seus valors inicials. Si hi passes un o més noms de camp, nomes reseteja aquells camps. De manera similar, form.clearErrors() neteja tots els errors o nomes els dels camps especificats.
Transformar dades abans d'enviar#
De vegades necessites modificar les dades del formulari abans d'enviar-les al servidor. El mètode transform permet fer-ho sense modificar l'estat visible del formulari:
<script setup>
import { useForm } from '@inertiajs/vue3';
const form = useForm({
name: '',
date: '',
remember: true,
});
function submit() {
form.transform((data) => ({
...data,
date: data.date ? new Date(data.date).toISOString() : null,
remember: data.remember ? 'on' : '',
})).post('/events');
}
</script>Exemple complet de CRUD#
Per veure com encaixen totes les peces, vegem un exemple complet d'edició d'un article. Primer, el controlador:
class ArticleController extends Controller
{
public function edit(Article $article)
{
return Inertia::render('Articles/Edit', [
'article' => $article->only('id', 'title', 'body', 'category_id', 'published'),
'categories' => Category::select('id', 'name')->get(),
]);
}
public function update(Request $request, Article $article)
{
$validated = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
'category_id' => 'nullable|exists:categories,id',
'published' => 'boolean',
]);
$article->update($validated);
return redirect()
->route('articles.show', $article)
->with('success', 'Article actualitzat correctament.');
}
public function destroy(Article $article)
{
$article->delete();
return redirect()
->route('articles.index')
->with('success', 'Article eliminat correctament.');
}
}I el component Vue corresponent:
<!-- resources/js/Pages/Articles/Edit.vue -->
<script setup>
import { useForm, Link, router } from '@inertiajs/vue3';
const props = defineProps({
article: Object,
categories: Array,
});
const form = useForm({
title: props.article.title,
body: props.article.body,
category_id: props.article.category_id,
published: props.article.published,
});
function submit() {
form.put(`/articles/${props.article.id}`, {
preserveScroll: true,
});
}
function deleteArticle() {
if (confirm('Segur que vols eliminar aquest article?')) {
router.delete(`/articles/${props.article.id}`);
}
}
</script>
<template>
<div>
<h1>Editar article</h1>
<form @submit.prevent="submit">
<div>
<label for="title">Titol</label>
<input id="title" v-model="form.title" type="text" />
<p v-if="form.errors.title" class="text-red-600">
{{ form.errors.title }}
</p>
</div>
<div>
<label for="body">Contingut</label>
<textarea id="body" v-model="form.body"></textarea>
<p v-if="form.errors.body" class="text-red-600">
{{ form.errors.body }}
</p>
</div>
<div>
<label for="category">Categoria</label>
<select id="category" v-model="form.category_id">
<option :value="null">Sense categoria</option>
<option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</option>
</select>
</div>
<div>
<label>
<input v-model="form.published" type="checkbox" />
Publicat
</label>
</div>
<div>
<button type="submit" :disabled="form.processing">
Actualitzar article
</button>
<button type="button" @click="deleteArticle" class="text-red-600">
Eliminar
</button>
</div>
</form>
<Link href="/articles">Tornar als articles</Link>
</div>
</template>Layouts#
En una aplicació real, la majoria de les pàgines comparteixen elements comuns com la capçalera, el menú de navegació i el peu de pàgina. Copiar i enganxar aquests elements a cada component de pàgina seria ineficient i difícil de mantenir. Inertia resol això amb layouts persistents.
Layouts persistents#
Un layout persistent és un component que embolcalla les pàgines i no es re-renderitza quan l'usuari navega entre pàgines. Això vol dir que l'estat del layout (per exemple, un reproductor d'àudio, la posició del scroll del menú lateral, o un comptador de notificacions amb WebSocket) es manté intacte durant tota la navegació.
Primer, crea el component de layout:
<!-- resources/js/Layouts/AppLayout.vue -->
<script setup>
import { Link, usePage } from '@inertiajs/vue3';
import { computed } from 'vue';
const page = usePage();
const user = computed(() => page.props.auth.user);
</script>
<template>
<div class="min-h-screen">
<header class="bg-white shadow">
<nav class="container mx-auto px-4 py-3 flex justify-between">
<div class="flex space-x-4">
<Link href="/" class="font-bold">La meva App</Link>
<Link href="/articles">Articles</Link>
<Link href="/dashboard" v-if="user">Dashboard</Link>
</div>
<div v-if="user">
<span>{{ user.name }}</span>
<Link href="/logout" method="post" as="button">Sortir</Link>
</div>
<div v-else>
<Link href="/login">Entrar</Link>
</div>
</nav>
</header>
<main class="container mx-auto px-4 py-8">
<slot />
</main>
<footer class="bg-gray-100 mt-16 py-8 text-center text-gray-500">
<p>© 2024 La meva App. Tots els drets reservats.</p>
</footer>
</div>
</template>Assignar layouts a les pagines#
Per indicar quin layout ha d'utilitzar una pàgina, defineix la propietat layout al component. Hi ha dues maneres de fer-ho. La primera és dins del bloc <script setup>:
<!-- resources/js/Pages/Articles/Index.vue -->
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
defineOptions({ layout: AppLayout });
defineProps({
articles: Object,
});
</script>
<template>
<div>
<h1>Articles</h1>
<!-- contingut de la pagina -->
</div>
</template>Layout per defecte#
Si la majoria de les teves pàgines utilitzen el mateix layout, pots configurar-lo com a layout per defecte a la inicialització de l'aplicació, evitant haver-lo d'importar a cada pàgina:
// resources/js/app.js
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import AppLayout from './Layouts/AppLayout.vue';
createInertiaApp({
resolve: (name) => {
const page = resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob('./Pages/**/*.vue')
);
page.then((module) => {
module.default.layout = module.default.layout || AppLayout;
});
return page;
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el);
},
});Amb aquesta configuració, totes les pàgines utilitzen AppLayout per defecte. Les pàgines que necessiten un layout diferent (o cap layout) poden sobreescriure'l explícitament.
Layouts imbricats#
Per a aplicacions complexes, pots imbricar layouts. Per exemple, una secció d'administració que té el seu propi layout dins del layout principal:
<!-- resources/js/Layouts/AdminLayout.vue -->
<script setup>
import { Link } from '@inertiajs/vue3';
</script>
<template>
<div class="flex">
<aside class="w-64 bg-gray-800 text-white min-h-screen p-4">
<h2 class="text-lg font-bold mb-4">Administracio</h2>
<nav class="space-y-2">
<Link href="/admin/dashboard" class="block">Dashboard</Link>
<Link href="/admin/users" class="block">Usuaris</Link>
<Link href="/admin/articles" class="block">Articles</Link>
<Link href="/admin/settings" class="block">Configuracio</Link>
</nav>
</aside>
<div class="flex-1 p-8">
<slot />
</div>
</div>
</template><!-- resources/js/Pages/Admin/Users.vue -->
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import AdminLayout from '@/Layouts/AdminLayout.vue';
defineOptions({ layout: [AppLayout, AdminLayout] });
defineProps({
users: Array,
});
</script>
<template>
<div>
<h1>Gestio d'usuaris</h1>
<!-- contingut -->
</div>
</template>Quan defineixes un array de layouts, el primer element es el layout exterior i el segon es l'interior. La pàgina es renderitzara dins de AdminLayout, que al seu torn es renderitzara dins d'AppLayout.
Gestio del scroll#
El comportament del scroll es un detall que pot marcar la diferència entre una aplicació que se sent polida i una que resulta desconcertant. Inertia proporciona diverses eines per controlar el scroll durant la navegació.
Per defecte, Inertia restableix la posició del scroll a dalt de tot cada vegada que navegues a una nova pàgina. Això es el comportament esperat en la majoria de casos, però hi ha situacions on vols mantenir la posició, especialment quan apliques filtres, canvies d'ordre o navegues entre pestanyes dins de la mateixa pàgina.
L'opció preserveScroll es pot utilitzar tant al component Link com a les navegacions programàtiques:
<template>
<Link href="/articles?status=published" preserve-scroll>
Publicats
</Link>
</template><script setup>
import { router } from '@inertiajs/vue3';
function applyFilter(filter) {
router.get('/articles', { filter }, {
preserveScroll: true,
});
}
</script>Tambe pots definir regions de scroll dins de la teva aplicació. Això es útil quan tens un contenidor amb scroll propi (com un panell lateral o una llista amb scroll independent) i vols que Inertia restauri la posició del scroll d'aquell contenidor específic en lloc del scroll de la pàgina completa:
<template>
<div class="overflow-y-auto h-screen" scroll-region>
<!-- contingut amb scroll -->
</div>
</template>L'atribut scroll-region indica a Inertia que ha de rastrejar i restaurar la posició del scroll d'aquest element quan l'usuari navega endavant i enrere amb els botons del navegador.
Server-Side Rendering (SSR)#
El Server-Side Rendering es una funcionalitat que permet que la teva aplicació Inertia renderitzi les pàgines al servidor abans d'enviar-les al navegador. Això te dos avantatges principals: millora el SEO, ja que els motors de cerca reben HTML complet en lloc d'una pàgina buida amb JavaScript, i millora el rendiment percebut, ja que l'usuari veu el contingut immediatament sense esperar que JavaScript es carregui i s'executi.
Per que el SSR es important#
Sense SSR, una aplicació Inertia funciona aixi: el navegador rep una pàgina HTML gairebé buida, descarrega i executa el bundle de JavaScript, i aleshores renderitza el contingut. Això vol dir que durant uns segons (depenent de la mida del bundle i la velocitat de connexió), l'usuari veu una pàgina en blanc. Amb SSR, el servidor pre-renderitza el HTML del component, l'envia al navegador, i l'usuari veu el contingut immediatament. Quan JavaScript es carrega, "hidrata" el HTML existent i l'aplicació es torna interactiva.
Configurar SSR amb Inertia i Laravel#
Primer, instal·la les dependències necessàries:
npm install @vue/server-rendererCrea el fitxer d'entrada per al servidor SSR a resources/js/ssr.js:
// resources/js/ssr.js
import { createInertiaApp } from '@inertiajs/vue3';
import createServer from '@inertiajs/vue3/server';
import { renderToString } from '@vue/server-renderer';
import { createSSRApp, h } from 'vue';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
createServer((page) =>
createInertiaApp({
page,
render: renderToString,
resolve: (name) => resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob('./Pages/**/*.vue')
),
setup({ App, props, plugin }) {
return createSSRApp({ render: () => h(App, props) })
.use(plugin);
},
})
);Observa que aqui fem servir createSSRApp en lloc de createApp, i passem la funcio renderToString al paràmetre render. Tambe es important notar que no muntem l'aplicacio en cap element del DOM, ja que el procés SSR genera HTML com a string.
Ara afegeix l'entrada SSR a la configuració de Vite:
// 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/js/app.js',
ssr: 'resources/js/ssr.js',
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
});Activa SSR a la configuració d'Inertia:
// config/inertia.php
return [
'ssr' => [
'enabled' => true,
'url' => 'http://127.0.0.1:13714',
],
];Compilar i executar#
Per compilar els assets SSR, executa la comanda de build de Vite amb l'opció SSR:
npm run buildInertia compila l'entrada SSR automàticament si esta configurada a Vite. Després, inicia el servidor SSR de Node.js:
php artisan inertia:start-ssrAquesta comanda arrenca un procés de Node.js que escolta peticions de renderitzat. El servidor de Laravel es comunica amb aquest procés per pre-renderitzar les pàgines. Per aturar el servidor SSR:
php artisan inertia:stop-ssrLimitacions del SSR#
Hi ha algunes consideracions importants quan treballes amb SSR. El codi que s'executa al servidor no te accés al DOM del navegador, aixi que no pots fer servir window, document, ni cap API del navegador dins del codi que s'executa durant el renderitzat inicial. Si necessites executar codi que depèn del navegador, embolcalla'l dins d'un hook onMounted de Vue, que nomes s'executa al client:
<script setup>
import { onMounted, ref } from 'vue';
const windowWidth = ref(0);
onMounted(() => {
windowWidth.value = window.innerWidth;
window.addEventListener('resize', () => {
windowWidth.value = window.innerWidth;
});
});
</script>Tambe cal tenir en compte que el procés SSR consumeix memòria addicional i pot tenir impacte en el rendiment del servidor si rep moltes peticions simultànies. Es recomanable monitoritzar el consum de recursos i considerar l'us de cache per a pàgines que no canvien sovint.
Autenticació i autoritzacio#
Un dels grans avantatges d'Inertia es que no necessites reinventar l'autenticació. Com que les peticions d'Inertia són peticions HTTP normals (amb cookies de sessió), pots fer servir el sistema d'autenticació estàndard de Laravel sense cap modificació. Les sessions, els guards, els middleware i les polítiques funcionen exactament com en una aplicació Blade tradicional.
Compartir l'usuari autenticat#
El patró més habitual es compartir les dades de l'usuari autenticat a través del middleware HandleInertiaRequests. Ja hem vist com fer-ho a la secció de dades compartides, pero vegem-ho amb mes detall:
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user()
? $request->user()->only('id', 'name', 'email', 'avatar')
: null,
'permissions' => $request->user()
? [
'canManageArticles' => $request->user()->can('manage', Article::class),
'canManageUsers' => $request->user()->can('manage', User::class),
'isAdmin' => $request->user()->hasRole('admin'),
]
: [],
],
];
}Nota que limitem les dades de l'usuari amb only() per no exposar informació sensible al client. Tambe afegim un objecte de permisos que indica les accions que l'usuari pot realitzar. Això permet condicionar la interfície d'acord amb els permisos de l'usuari.
Protegir rutes amb middleware#
Les rutes es protegeixen exactament com en qualsevol aplicació Laravel:
// routes/web.php
Route::middleware('auth')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::resource('articles', ArticleController::class);
});
Route::middleware('guest')->group(function () {
Route::get('/login', [AuthController::class, 'showLogin']);
Route::post('/login', [AuthController::class, 'login']);
});Quan un usuari no autenticat intenta accedir a una ruta protegida, Laravel el redirigeix a la pàgina de login. Inertia gestiona aquesta redirecció de forma transparent: intercepta la resposta 302, fa una nova petició a l'URL de redirecció, i renderitza el component de login sense recàrrega de pàgina.
Redirecció després del login#
El controlador de login tambe funciona com una aplicació tradicional. Després d'autenticar l'usuari, simplement fas una redirecció:
class AuthController extends Controller
{
public function showLogin()
{
return Inertia::render('Auth/Login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials, $request->boolean('remember'))) {
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
throw ValidationException::withMessages([
'email' => 'Les credencials no son correctes.',
]);
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}El component de login:
<!-- resources/js/Pages/Auth/Login.vue -->
<script setup>
import { useForm } from '@inertiajs/vue3';
const form = useForm({
email: '',
password: '',
remember: false,
});
function submit() {
form.post('/login', {
onFinish: () => form.reset('password'),
});
}
</script>
<template>
<div class="max-w-md mx-auto mt-16">
<h1 class="text-2xl font-bold mb-6">Iniciar sessio</h1>
<form @submit.prevent="submit">
<div class="mb-4">
<label for="email">Correu electronic</label>
<input
id="email"
v-model="form.email"
type="email"
class="w-full border rounded px-3 py-2"
/>
<p v-if="form.errors.email" class="text-red-600 text-sm mt-1">
{{ form.errors.email }}
</p>
</div>
<div class="mb-4">
<label for="password">Contrasenya</label>
<input
id="password"
v-model="form.password"
type="password"
class="w-full border rounded px-3 py-2"
/>
</div>
<div class="mb-4">
<label class="flex items-center">
<input v-model="form.remember" type="checkbox" class="mr-2" />
Recorda'm
</label>
</div>
<button
type="submit"
:disabled="form.processing"
class="w-full bg-blue-600 text-white py-2 rounded"
>
Entrar
</button>
</form>
</div>
</template>Fixa't com onFinish: () => form.reset('password') neteja el camp de contrasenya un cop la petició s'ha completat, independentment de si ha tingut èxit o no. Això es una bona pràctica de seguretat per no deixar la contrasenya al formulari.
Paginació#
La paginació amb Inertia aprofita el sistema de paginació integrat de Laravel. No necessites cap llibreria addicional al client: simplement passes les dades paginades des del controlador i les renderitzes al component.
Passar dades paginades des del controlador#
Quan crides paginate() en una consulta d'Eloquent, Laravel retorna un objecte LengthAwarePaginator que inclou les dades, els links de paginació, el nombre total de resultats i altra informació útil. Inertia serialitza automàticament aquest objecte a JSON:
public function index(Request $request)
{
return Inertia::render('Articles/Index', [
'articles' => Article::query()
->with('author:id,name')
->when($request->input('search'), function ($query, $search) {
$query->where('title', 'like', "%{$search}%");
})
->when($request->input('category'), function ($query, $category) {
$query->where('category_id', $category);
})
->published()
->latest()
->paginate(15)
->withQueryString(),
'filters' => $request->only(['search', 'category']),
]);
}El mètode withQueryString() es important: fa que els links de paginació mantinguin els paràmetres de cerca actuals. Sense ell, en fer clic a la pàgina 2 es perdrien els filtres aplicats.
Renderitzar la paginació al component#
L'objecte paginat que arriba al component te l'estructura estàndard de Laravel: data conté els resultats, links conté els links de paginació, i meta conté informació com el total de resultats i la pàgina actual. Pots renderitzar-ho aixi:
<!-- resources/js/Pages/Articles/Index.vue -->
<script setup>
import { Link, router } from '@inertiajs/vue3';
import { ref, watch } from 'vue';
const props = defineProps({
articles: Object,
filters: Object,
});
const search = ref(props.filters.search || '');
watch(search, (value) => {
router.get('/articles', { search: value }, {
preserveState: true,
preserveScroll: true,
replace: true,
});
});
</script>
<template>
<div>
<h1>Articles</h1>
<input
v-model="search"
type="text"
placeholder="Cercar articles..."
class="border rounded px-3 py-2 mb-6 w-full"
/>
<div v-for="article in articles.data" :key="article.id" class="mb-4">
<Link :href="`/articles/${article.id}`">
<h2 class="text-xl font-bold">{{ article.title }}</h2>
</Link>
<p class="text-gray-500">Per {{ article.author.name }}</p>
</div>
<nav v-if="articles.links.length > 3" class="flex space-x-1 mt-8">
<Link
v-for="link in articles.links"
:key="link.label"
:href="link.url"
class="px-3 py-1 border rounded"
:class="{
'bg-blue-600 text-white': link.active,
'text-gray-400 pointer-events-none': !link.url,
}"
v-html="link.label"
preserve-scroll
/>
</nav>
<p class="text-gray-500 mt-4">
Mostrant {{ articles.from }} a {{ articles.to }}
de {{ articles.total }} resultats
</p>
</div>
</template>L'array articles.links que proporciona el paginador de Laravel conte objectes amb url, label i active. El component Link d'Inertia intercepta els clics en els links de paginació i fa les transicions via XHR, mantenint l'experiència de SPA. Fixa't que cada link te preserve-scroll per evitar que la pàgina salti al principi en canviar de pàgina.
L'us de v-html="link.label" es necessari perquè els labels de paginació de Laravel inclouen entitats HTML (com « i » per als botons anterior i segent).
Tambe pots crear un component de paginació reutilitzable per no repetir aquest codi a cada pàgina que necesiti paginació:
<!-- resources/js/Components/Pagination.vue -->
<script setup>
import { Link } from '@inertiajs/vue3';
defineProps({
links: Array,
from: Number,
to: Number,
total: Number,
});
</script>
<template>
<div>
<nav v-if="links.length > 3" class="flex space-x-1">
<Link
v-for="link in links"
:key="link.label"
:href="link.url"
class="px-3 py-1 border rounded"
:class="{
'bg-blue-600 text-white': link.active,
'text-gray-400 pointer-events-none': !link.url,
}"
v-html="link.label"
preserve-scroll
/>
</nav>
<p v-if="total" class="text-gray-500 mt-2">
Mostrant {{ from }} a {{ to }} de {{ total }} resultats
</p>
</div>
</template>I fer-lo servir a qualsevol pàgina:
<template>
<Pagination
:links="articles.links"
:from="articles.from"
:to="articles.to"
:total="articles.total"
/>
</template>Amb tot això, tens una base sòlida per construir aplicacions completes amb Inertia.js i Laravel. La combinació permet mantenir la simplicitat del desenvolupament monolític amb Laravel: rutes, controladors, validació, autenticació, sessions i tota la potència del framework al servidor, i al mateix temps oferir una experiència d'usuari moderna i fluida gràcies al renderitzat de components Vue o React sense recàrregues de pàgina.