«

使用laravel12 inertiajs vue3 开发项目笔记

时间:2025-6-20 21:22     作者:wanzi     分类: php


Vue3 与 Inertia.js 使用指南

概述

Inertia.js 是一种构建单页应用的现代方法,允许你使用传统的服务器端路由和控制器创建完全客户端渲染的单页应用,无需构建API。

1. Inertia.js 协议原理

核心概念

根据 Inertia.js 官方协议,Inertia 工作原理如下:

  1. 首次请求:返回完整的HTML文档
  2. 后续请求:通过AJAX返回JSON响应,包含组件名称和数据
  3. 客户端渲染:使用JSON数据渲染对应的Vue组件

请求流程

用户访问 /users
    ↓ (首次访问)
Laravel 返回完整HTML + Inertia 数据
    ↓
Vue.js 接管页面
    ↓ (点击链接)
发送带 X-Inertia 头的AJAX请求
    ↓
Laravel 返回 JSON: { component: 'Users/Index', props: {...} }
    ↓
Vue.js 使用数据渲染新组件

HTTP 头部

Inertia 使用特定的HTTP头部进行通信:

2. 安装配置

后端配置 (Laravel)

composer require inertiajs/inertia-laravel
php artisan inertia:middleware
// app/Http/Kernel.php
protected $middlewareGroups = [
    'web' => [
        // ...
        \App\Http\Middleware\HandleInertiaRequests::class,
    ],
];

前端配置 (Vue3)

npm install @inertiajs/vue3
npm install @vitejs/plugin-vue
// resources/js/app.js
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'

createInertiaApp({
  resolve: name => {
    const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
    return pages[`./Pages/${name}.vue`]
  },
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .mount(el)
  },
})

3. 完整Demo:用户列表管理

这是一个完整的前后端示例,展示用户列表的增删改查功能。

3.1 后端控制器

<?php
// app/Http/Controllers/UserController.php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;

class UserController extends Controller
{
    /**
     * 用户列表页面
     */
    public function index(Request $request)
    {
        $users = User::query()
            ->when($request->search, function ($query, $search) {
                $query->where('name', 'like', "%{$search}%")
                      ->orWhere('email', 'like', "%{$search}%");
            })
            ->paginate(10)
            ->withQueryString();

        return Inertia::render('Users/Index', [
            'users' => $users,
            'filters' => $request->only(['search']),
            'can' => [
                'create_user' => auth()->user()->can('create', User::class),
                'edit_user' => auth()->user()->can('update', User::class),
            ],
        ]);
    }

    /**
     * 显示创建用户表单
     */
    public function create()
    {
        return Inertia::render('Users/Create', [
            'roles' => ['admin', 'user'],
        ]);
    }

    /**
     * 存储新用户
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'role' => 'required|in:admin,user',
            'password' => 'required|string|min:8|confirmed',
        ]);

        User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'role' => $validated['role'],
            'password' => bcrypt($validated['password']),
        ]);

        return redirect()->route('users.index')
            ->with('success', '用户创建成功');
    }

    /**
     * 显示编辑用户表单
     */
    public function edit(User $user)
    {
        return Inertia::render('Users/Edit', [
            'user' => $user,
            'roles' => ['admin', 'user'],
        ]);
    }

    /**
     * 更新用户
     */
    public function update(Request $request, User $user)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users,email,' . $user->id,
            'role' => 'required|in:admin,user',
        ]);

        $user->update($validated);

        return redirect()->route('users.index')
            ->with('success', '用户更新成功');
    }

    /**
     * 删除用户
     */
    public function destroy(User $user)
    {
        $user->delete();

        return redirect()->route('users.index')
            ->with('success', '用户删除成功');
    }
}

3.2 路由配置

<?php
// routes/web.php

use App\Http\Controllers\UserController;

Route::middleware(['auth'])->group(function () {
    Route::resource('users', UserController::class);
});

3.3 前端页面组件

用户列表页面

<!-- resources/js/Pages/Users/Index.vue -->
<template>
  <AppLayout title="用户管理">
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        用户管理
      </h2>
    </template>

    <div class="py-12">
      <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">

          <!-- 工具栏 -->
          <div class="p-6 border-b border-gray-200">
            <div class="flex justify-between items-center">
              <div class="flex items-center space-x-4">
                <!-- 搜索框 -->
                <div class="relative">
                  <input
                    v-model="search"
                    type="text"
                    placeholder="搜索用户..."
                    class="block w-64 pl-10 pr-3 py-2 border border-gray-300 rounded-md"
                    @keyup.enter="searchUsers"
                  />
                  <div class="absolute inset-y-0 left-0 pl-3 flex items-center">
                    <svg class="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
                      <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
                    </svg>
                  </div>
                </div>
                <button @click="searchUsers" class="px-4 py-2 bg-gray-500 text-white rounded">
                  搜索
                </button>
                <button @click="clearSearch" class="px-4 py-2 bg-gray-300 text-gray-700 rounded">
                  清除
                </button>
              </div>

              <!-- 创建按钮 -->
              <Link
                v-if="can.create_user"
                :href="route('users.create')"
                class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
              >
                创建用户
              </Link>
            </div>
          </div>

          <!-- 用户表格 -->
          <div class="overflow-x-auto">
            <table class="min-w-full divide-y divide-gray-200">
              <thead class="bg-gray-50">
                <tr>
                  <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    用户信息
                  </th>
                  <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    角色
                  </th>
                  <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    创建时间
                  </th>
                  <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
                    操作
                  </th>
                </tr>
              </thead>
              <tbody class="bg-white divide-y divide-gray-200">
                <tr v-for="user in users.data" :key="user.id" class="hover:bg-gray-50">
                  <td class="px-6 py-4 whitespace-nowrap">
                    <div class="flex items-center">
                      <div class="flex-shrink-0 h-10 w-10">
                        <img class="h-10 w-10 rounded-full" :src="user.avatar || '/default-avatar.png'" :alt="user.name" />
                      </div>
                      <div class="ml-4">
                        <div class="text-sm font-medium text-gray-900">{{ user.name }}</div>
                        <div class="text-sm text-gray-500">{{ user.email }}</div>
                      </div>
                    </div>
                  </td>
                  <td class="px-6 py-4 whitespace-nowrap">
                    <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
                          :class="user.role === 'admin' ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'">
                      {{ user.role === 'admin' ? '管理员' : '普通用户' }}
                    </span>
                  </td>
                  <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                    {{ formatDate(user.created_at) }}
                  </td>
                  <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                    <div class="flex justify-end space-x-2">
                      <Link
                        v-if="can.edit_user"
                        :href="route('users.edit', user.id)"
                        class="text-indigo-600 hover:text-indigo-900"
                      >
                        编辑
                      </Link>
                      <button
                        v-if="can.edit_user && user.id !== $page.props.auth.user.id"
                        @click="deleteUser(user)"
                        class="text-red-600 hover:text-red-900"
                      >
                        删除
                      </button>
                    </div>
                  </td>
                </tr>
              </tbody>
            </table>
          </div>

          <!-- 分页 -->
          <div class="px-6 py-4 border-t border-gray-200">
            <Pagination :links="users.links" />
          </div>
        </div>
      </div>
    </div>
  </AppLayout>
</template>

<script setup>
import { ref, watch } from 'vue'
import { Link, router, usePage } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
import Pagination from '@/Components/Pagination.vue'

// 接收控制器传递的数据
const props = defineProps({
  users: Object,
  filters: Object,
  can: Object,
})

// 响应式数据
const search = ref(props.filters.search || '')

// 搜索用户
const searchUsers = () => {
  router.get(route('users.index'), { search: search.value }, {
    preserveState: true,
    replace: true,
  })
}

// 清除搜索
const clearSearch = () => {
  search.value = ''
  searchUsers()
}

// 删除用户
const deleteUser = (user) => {
  if (confirm(`确认删除用户 "${user.name}" 吗?`)) {
    router.delete(route('users.destroy', user.id), {
      onSuccess: () => {
        console.log('用户删除成功')
      }
    })
  }
}

// 格式化日期
const formatDate = (dateString) => {
  return new Date(dateString).toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit'
  })
}

// 监听页面props变化
const page = usePage()
watch(() => page.props.flash.success, (message) => {
  if (message) {
    alert(message) // 实际项目中应该使用Toast组件
  }
})
</script>

创建用户表单

<!-- resources/js/Pages/Users/Create.vue -->
<template>
  <AppLayout title="创建用户">
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        创建用户
      </h2>
    </template>

    <div class="py-12">
      <div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
          <form @submit.prevent="submit" class="p-6 space-y-6">

            <!-- 用户名 -->
            <div>
              <label for="name" class="block text-sm font-medium text-gray-700">
                用户名 <span class="text-red-500">*</span>
              </label>
              <input
                id="name"
                v-model="form.name"
                type="text"
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                :class="{ 'border-red-500': form.errors.name }"
                placeholder="请输入用户名"
              />
              <div v-if="form.errors.name" class="mt-1 text-sm text-red-500">
                {{ form.errors.name }}
              </div>
            </div>

            <!-- 邮箱 -->
            <div>
              <label for="email" class="block text-sm font-medium text-gray-700">
                邮箱地址 <span class="text-red-500">*</span>
              </label>
              <input
                id="email"
                v-model="form.email"
                type="email"
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                :class="{ 'border-red-500': form.errors.email }"
                placeholder="请输入邮箱地址"
              />
              <div v-if="form.errors.email" class="mt-1 text-sm text-red-500">
                {{ form.errors.email }}
              </div>
            </div>

            <!-- 角色 -->
            <div>
              <label for="role" class="block text-sm font-medium text-gray-700">
                用户角色 <span class="text-red-500">*</span>
              </label>
              <select
                id="role"
                v-model="form.role"
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                :class="{ 'border-red-500': form.errors.role }"
              >
                <option value="">请选择角色</option>
                <option v-for="role in roles" :key="role" :value="role">
                  {{ role === 'admin' ? '管理员' : '普通用户' }}
                </option>
              </select>
              <div v-if="form.errors.role" class="mt-1 text-sm text-red-500">
                {{ form.errors.role }}
              </div>
            </div>

            <!-- 密码 -->
            <div>
              <label for="password" class="block text-sm font-medium text-gray-700">
                密码 <span class="text-red-500">*</span>
              </label>
              <input
                id="password"
                v-model="form.password"
                type="password"
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                :class="{ 'border-red-500': form.errors.password }"
                placeholder="请输入密码(至少8位)"
              />
              <div v-if="form.errors.password" class="mt-1 text-sm text-red-500">
                {{ form.errors.password }}
              </div>
            </div>

            <!-- 确认密码 -->
            <div>
              <label for="password_confirmation" class="block text-sm font-medium text-gray-700">
                确认密码 <span class="text-red-500">*</span>
              </label>
              <input
                id="password_confirmation"
                v-model="form.password_confirmation"
                type="password"
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                placeholder="请再次输入密码"
              />
            </div>

            <!-- 按钮组 -->
            <div class="flex items-center justify-end space-x-4">
              <Link
                :href="route('users.index')"
                class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded"
              >
                取消
              </Link>
              <button
                type="submit"
                :disabled="form.processing"
                class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
              >
                {{ form.processing ? '创建中...' : '创建用户' }}
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </AppLayout>
</template>

<script setup>
import { useForm, Link } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'

const props = defineProps({
  roles: Array,
})

// 创建表单
const form = useForm({
  name: '',
  email: '',
  role: '',
  password: '',
  password_confirmation: '',
})

// 提交表单
const submit = () => {
  form.post(route('users.store'), {
    onSuccess: () => {
      console.log('用户创建成功')
      // 成功后会自动重定向到 users.index
    },
    onError: (errors) => {
      console.error('创建失败:', errors)
      // 错误会自动绑定到 form.errors
    },
  })
}
</script>

编辑用户表单

<!-- resources/js/Pages/Users/Edit.vue -->
<template>
  <AppLayout title="编辑用户">
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        编辑用户: {{ user.name }}
      </h2>
    </template>

    <div class="py-12">
      <div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
          <form @submit.prevent="submit" class="p-6 space-y-6">

            <!-- 用户名 -->
            <div>
              <label for="name" class="block text-sm font-medium text-gray-700">
                用户名 <span class="text-red-500">*</span>
              </label>
              <input
                id="name"
                v-model="form.name"
                type="text"
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                :class="{ 'border-red-500': form.errors.name }"
              />
              <div v-if="form.errors.name" class="mt-1 text-sm text-red-500">
                {{ form.errors.name }}
              </div>
            </div>

            <!-- 邮箱 -->
            <div>
              <label for="email" class="block text-sm font-medium text-gray-700">
                邮箱地址 <span class="text-red-500">*</span>
              </label>
              <input
                id="email"
                v-model="form.email"
                type="email"
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                :class="{ 'border-red-500': form.errors.email }"
              />
              <div v-if="form.errors.email" class="mt-1 text-sm text-red-500">
                {{ form.errors.email }}
              </div>
            </div>

            <!-- 角色 -->
            <div>
              <label for="role" class="block text-sm font-medium text-gray-700">
                用户角色 <span class="text-red-500">*</span>
              </label>
              <select
                id="role"
                v-model="form.role"
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                :class="{ 'border-red-500': form.errors.role }"
              >
                <option value="">请选择角色</option>
                <option v-for="role in roles" :key="role" :value="role">
                  {{ role === 'admin' ? '管理员' : '普通用户' }}
                </option>
              </select>
              <div v-if="form.errors.role" class="mt-1 text-sm text-red-500">
                {{ form.errors.role }}
              </div>
            </div>

            <!-- 按钮组 -->
            <div class="flex items-center justify-end space-x-4">
              <Link
                :href="route('users.index')"
                class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded"
              >
                取消
              </Link>
              <button
                type="submit"
                :disabled="form.processing"
                class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
              >
                {{ form.processing ? '更新中...' : '更新用户' }}
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </AppLayout>
</template>

<script setup>
import { useForm, Link } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'

const props = defineProps({
  user: Object,
  roles: Array,
})

// 创建表单,预填充现有数据
const form = useForm({
  name: props.user.name,
  email: props.user.email,
  role: props.user.role,
})

// 提交表单
const submit = () => {
  form.put(route('users.update', props.user.id), {
    onSuccess: () => {
      console.log('用户更新成功')
    },
    onError: (errors) => {
      console.error('更新失败:', errors)
    },
  })
}
</script>

4. 关键概念与最佳实践

4.1 数据传递

控制器到Vue组件

return Inertia::render('Users/Index', [
    'users' => $users,
    'filters' => $request->only(['search']),
    'can' => [
        'create_user' => auth()->user()->can('create', User::class),
    ],
]);

Vue组件接收

<script setup>
const props = defineProps({
  users: Object,
  filters: Object,
  can: Object,
})
</script>

4.2 表单处理

使用 useForm 进行表单状态管理:

<script setup>
import { useForm } from '@inertiajs/vue3'

const form = useForm({
  name: '',
  email: '',
})

const submit = () => {
  form.post('/users', {
    onSuccess: () => console.log('Success'),
    onError: (errors) => console.log('Errors:', errors),
  })
}
</script>

4.3 导航

使用 Link 组件

<template>
  <!-- 基础导航 -->
  <Link href="/users">用户列表</Link>

  <!-- 带参数 -->
  <Link :href="route('users.edit', user.id)">编辑</Link>

  <!-- POST/DELETE 请求 -->
  <Link href="/logout" method="post" as="button">退出</Link>
</template>

4.4 部分重载

只更新页面的特定部分:

<script setup>
import { router } from '@inertiajs/vue3'

const refreshStats = () => {
  router.reload({ only: ['stats'] })
}
</script>

4.5 全局数据共享

通过中间件共享数据:

// HandleInertiaRequests.php
public function share(Request $request): array
{
    return [
        'auth' => [
            'user' => $request->user(),
        ],
        'flash' => [
            'success' => fn () => $request->session()->get('success'),
            'error' => fn () => $request->session()->get('error'),
        ],
    ];
}

5. 性能优化

5.1 懒加载数据

return Inertia::render('Dashboard', [
    'stats' => $basicStats,
    'heavyData' => Inertia::lazy(fn () => $this->getHeavyData()),
]);

5.2 代码分割

<script setup>
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent(() => 
  import('./HeavyComponent.vue')
)
</script>

6. 常见问题

6.1 表单验证错误

确保正确处理验证错误:

<template>
  <input v-model="form.name" :class="{ 'border-red-500': form.errors.name }" />
  <div v-if="form.errors.name" class="text-red-500">
    {{ form.errors.name }}
  </div>
</template>

6.2 权限检查

在组件中检查用户权限:

<script setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'

const page = usePage()
const canEdit = computed(() => page.props.can.edit_user)
</script>

<template>
  <button v-if="canEdit">编辑</button>
</template>

多入口实现

我们要实现门户端和管理端在同一项目里面,这个时候要求门户端启用服务器渲染ssr,管理端不启用,要如何做

分析

对于这个问题,也就是要求我们要有不同的入口视图文件或者不同的入口js。那针对这样的考虑,我们可以从两个方面去考虑

1、入口js判断

通过在app.blade.php进行路由分发判断

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title inertia>{{ config('app.name', 'Laravel') }}</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />

        <!-- Scripts -->
        @routes
        @if (request ()->is ('admin/*')) @vite (['resources/js/admin.app.js', "resources/js/Pages/Admin/{$page ['component']}.vue"]) 
        @else @vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"]) 
        @endif
        @inertiaHead
    </head>
    <body class="font-sans antialiased">
        @inertia
    </body>
</html>

我们响应的在vite.config.js里面增加热更新配置

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

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

2、通过中间件判断

在一个网友的分享下,我也认真的看了源码,我们其实可以通过HandleInertiaRequests 这个中间件 判断:


     // @param string $rootView 在第一次页面访问时加载的根模板。

    public function setRootView(string $rootView): void
    {
        if (\request()->routeIs('admin.*')) {
            $rootView = 'admin.app';
        }  else {
            $rootView = 'app';
        }
        $this->rootView = $rootView;
    }
rootView

设置将在访问应用程序的第一页时加载的根模板。这将用于加载您的网站资源(CSS和JavaScript),还将包含一个根<div>,用于启动您的JavaScript应用程序。

<!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>

此模板应包括您的静态资源,以及@inertia 和@inertiaHead指令。
指令解析的源码我可以放出来看下:vendor/inertiajs/inertia-laravel/src/Directive.php

<?php

namespace Inertia;

class Directive
{
    /**
     * Compiles the "@inertia" directive.
     *
     * @param  string  $expression
     */
    public static function compile($expression = ''): string
    {
        $id = trim(trim($expression), "\'\"") ?: 'app';

        $template = '<?php
            if (!isset($__inertiaSsrDispatched)) {
                $__inertiaSsrDispatched = true;
                $__inertiaSsrResponse = app(\Inertia\Ssr\Gateway::class)->dispatch($page);
            }

            if ($__inertiaSsrResponse) {
                echo $__inertiaSsrResponse->body;
            } else {
                ?><div id="'.$id.'" data-page="{{ json_encode($page) }}"></div><?php
            }
        ?>';

        return implode(' ', array_map('trim', explode("\n", $template)));
    }

    /**
     * Compiles the "@inertiaHead" directive.
     *
     * @param  string  $expression
     */
    public static function compileHead($expression = ''): string
    {
        $template = '<?php
            if (!isset($__inertiaSsrDispatched)) {
                $__inertiaSsrDispatched = true;
                $__inertiaSsrResponse = app(\Inertia\Ssr\Gateway::class)->dispatch($page);
            }

            if ($__inertiaSsrResponse) {
                echo $__inertiaSsrResponse->head;
            }
        ?>';

        return implode(' ', array_map('trim', explode("\n", $template)));
    }
}

然后再从我们可以从编译出视图缓存文件看它们实际的用处:

  1. @inertia:core app 根节点

  2. @inertiaHead :是 “惰性加载 SSR 响应,只在首次使用时触发,并缓存结果”。如果你项目没配置 SSR,也会跳过 SSR,因此它具有自动回退到客户端渲染的能力。是否使用 SSR,取决于你是否在 Laravel 中启用了 Inertia SSR 支持(通常是通过 Node.js 的 @inertiajs/server)。

<!DOCTYPE html>
<html lang="<?php echo e(str_replace('_', '-', app()->getLocale())); ?>">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title inertia><?php echo e(config('app.name', 'Laravel')); ?></title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />

        <!-- Scripts -->
        <?php echo app('Tighten\Ziggy\BladeRouteGenerator')->generate(); ?>
        <?php echo app('Illuminate\Foundation\Vite')(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"]); ?>
        <?php if (!isset($__inertiaSsrDispatched)) { $__inertiaSsrDispatched = true; $__inertiaSsrResponse = app(\Inertia\Ssr\Gateway::class)->dispatch($page); }  if ($__inertiaSsrResponse) { echo $__inertiaSsrResponse->head; } ?>
    </head>
    <body class="font-sans antialiased">
        <?php if (!isset($__inertiaSsrDispatched)) { $__inertiaSsrDispatched = true; $__inertiaSsrResponse = app(\Inertia\Ssr\Gateway::class)->dispatch($page); }  if ($__inertiaSsrResponse) { echo $__inertiaSsrResponse->body; } else { ?><div id="app" data-page="<?php echo e(json_encode($page)); ?>"></div><?php } ?>
    </body>
</html>
<?php /**PATH /Users/xxx/src/www/laravel-xxx-node/resources/views/app.blade.php ENDPATH**/ ?>

默认情况下,Inertia的Laravel适配器将假定您的根模板名为app.blade.php。如果您想使用不同的根视图,可以使用Inertia::setRootView()方法对其进行更改。

使用SSR

参考:https://inertiajs.com/server-side-rendering

总结

Inertia.js 提供了一种优雅的方式来构建现代单页应用,结合了传统服务器端渲染的简单性和现代客户端框架的用户体验。通过这个完整的用户管理Demo,你可以看到:

  1. 后端控制器负责数据准备和业务逻辑
  2. 前端组件负责用户界面和交互
  3. Inertia.js作为桥梁,无缝连接前后端
  4. 表单处理通过useForm简化状态管理
  5. 导航通过Link组件实现SPA式体验

这种架构让开发者能够专注于业务逻辑,而不必担心复杂的API设计和状态同步问题。