Laravel المتقدم

Inertia.js: المونوليث الحديث

18 دقيقة الدرس 38 من 40

Inertia.js: المونوليث الحديث

يسمح لك Inertia.js ببناء تطبيقات صفحة واحدة (SPAs) حديثة باستخدام التوجيه من جانب الخادم الكلاسيكي ووحدات التحكم. إنه يسد الفجوة بين تطبيقات Laravel التقليدية وأطر العمل الأمامية الحديثة مثل Vue.js أو React أو Svelte دون الحاجة إلى بناء API منفصل.

لماذا Inertia.js؟

  • لا حاجة لـ API: استخدم وحدات التحكم والمسارات من Laravel مباشرة
  • التوجيه من جانب الخادم: حدد المسارات في Laravel، وليس JavaScript
  • واجهة مستخدم حديثة: قم بالبناء باستخدام مكونات Vue أو React أو Svelte
  • تقسيم الكود التلقائي: قم بتحميل JavaScript المطلوب فقط لكل صفحة
  • إصدار الأصول: إزالة ذاكرة التخزين المؤقت تلقائيًا
  • البيانات المشتركة: تمرير البيانات من middleware إلى جميع الصفحات
ملاحظة: Inertia ليس بديلاً عن Laravel أو Vue/React. إنه الصمغ الذي يربطهم، مما يسمح لك باستخدام Laravel كخلفية و Vue/React كواجهة أمامية دون تعقيد بناء API منفصل.

التثبيت والإعداد

قم بتثبيت محولات Inertia من جانب الخادم وجانب العميل:

# تثبيت محول جانب الخادم composer require inertiajs/inertia-laravel # تثبيت محول جانب العميل (مثال Vue 3) npm install @inertiajs/vue3 # تثبيت Vue 3 npm install vue@next # تثبيت Vite plugin لـ Vue npm install @vitejs/plugin-vue

تكوين القالب الجذر و middleware:

<?php // app/Http/Middleware/HandleInertiaRequests.php namespace App\Http\Middleware; use Illuminate\Http\Request; use Inertia\Middleware; class HandleInertiaRequests extends Middleware { /** * القالب الجذر الذي يتم تحميله في أول زيارة للصفحة */ protected $rootView = 'app'; /** * يحدد إصدار الأصل الحالي */ public function version(Request $request): ?string { return parent::version($request); } /** * تعريف الخصائص المشتركة مع جميع الصفحات */ public function share(Request $request): array { return array_merge(parent::share($request), [ 'auth' => [ 'user' => $request->user() ? [ 'id' => $request->user()->id, 'name' => $request->user()->name, 'email' => $request->user()->email, 'avatar' => $request->user()->avatar_url, 'roles' => $request->user()->roles->pluck('name'), ] : null, ], 'flash' => [ 'success' => fn () => $request->session()->get('success'), 'error' => fn () => $request->session()->get('error'), 'warning' => fn () => $request->session()->get('warning'), ], 'errors' => fn () => $request->session()->get('errors') ? $request->session()->get('errors')->getBag('default')->getMessages() : (object) [], 'app' => [ 'name' => config('app.name'), 'locale' => app()->getLocale(), 'timezone' => config('app.timezone'), ], ]); } } // تسجيل middleware في app/Http/Kernel.php protected $middlewareGroups = [ 'web' => [ // ... middleware أخرى \App\Http\Middleware\HandleInertiaRequests::class, ], ];

القالب الجذر (Blade)

قم بإنشاء قالب Blade الذي يعمل كغلاف التطبيق:

<!-- resources/views/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') }}</title> <!-- الخطوط --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet"> <!-- السكريبتات --> @routes @vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"]) @inertiaHead </head> <body class="antialiased"> @inertia </body> </html>

إعداد الواجهة الأمامية (Vue 3)

قم بتكوين نقطة الدخول الخاصة بـ JavaScript:

// resources/js/app.js import { createApp, h } from 'vue' import { createInertiaApp } from '@inertiajs/vue3' import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m' createInertiaApp({ title: (title) => title ? `${title} - ${import.meta.env.VITE_APP_NAME}` : import.meta.env.VITE_APP_NAME, resolve: (name) => resolvePageComponent( `./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue') ), setup({ el, App, props, plugin }) { return createApp({ render: () => h(App, props) }) .use(plugin) .use(ZiggyVue) .mount(el) }, progress: { color: '#4B5563', showSpinner: true, }, })
نصيحة: استخدم resolvePageComponent() مع استيرادات Vite الديناميكية لتقسيم الكود التلقائي. سيتم تحميل كل مكون صفحة فقط عند الحاجة.

وحدات التحكم والاستجابات

قم بإرجاع استجابات Inertia من وحدات التحكم الخاصة بك:

<?php namespace App\Http\Controllers; use Inertia\Inertia; use App\Models\Post; use Illuminate\Http\Request; class PostController extends Controller { /** * عرض قائمة المقالات */ public function index() { return Inertia::render('Posts/Index', [ 'posts' => Post::with('author:id,name,avatar') ->latest() ->paginate(15) ->through(fn ($post) => [ 'id' => $post->id, 'title' => $post->title, 'excerpt' => $post->excerpt, 'published_at' => $post->published_at->format('M d, Y'), 'author' => [ 'name' => $post->author->name, 'avatar' => $post->author->avatar, ], 'can' => [ 'edit' => auth()->user()?->can('update', $post), 'delete' => auth()->user()?->can('delete', $post), ], ]), 'filters' => request()->only(['search', 'status', 'category']), ]); } /** * عرض نموذج إنشاء مقال جديد */ public function create() { return Inertia::render('Posts/Create', [ 'categories' => Category::select('id', 'name')->get(), 'tags' => Tag::select('id', 'name')->get(), ]); } /** * تخزين مقال جديد */ public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|max:255', 'content' => 'required', 'category_id' => 'required|exists:categories,id', 'tags' => 'array', 'tags.*' => 'exists:tags,id', 'featured_image' => 'nullable|image|max:2048', ]); $post = auth()->user()->posts()->create($validated); if ($request->hasFile('featured_image')) { $post->addMedia($request->file('featured_image')) ->toMediaCollection('featured'); } $post->tags()->sync($request->tags); return redirect() ->route('posts.show', $post) ->with('success', 'تم إنشاء المقال بنجاح!'); } /** * عرض مقال محدد */ public function show(Post $post) { return Inertia::render('Posts/Show', [ 'post' => [ 'id' => $post->id, 'title' => $post->title, 'content' => $post->content, 'published_at' => $post->published_at->format('F j, Y'), 'author' => [ 'name' => $post->author->name, 'avatar' => $post->author->avatar_url, 'bio' => $post->author->bio, ], 'category' => $post->category->only('id', 'name', 'slug'), 'tags' => $post->tags->map->only('id', 'name', 'slug'), 'featured_image' => $post->getFirstMediaUrl('featured'), 'can' => [ 'edit' => auth()->user()?->can('update', $post), 'delete' => auth()->user()?->can('delete', $post), ], ], 'related_posts' => Post::where('category_id', $post->category_id) ->where('id', '!=', $post->id) ->limit(3) ->get() ->map->only('id', 'title', 'excerpt', 'published_at'), ]); } }

مكونات Vue (الصفحات)

قم بإنشاء مكونات Vue لصفحات Inertia الخاصة بك:

<!-- resources/js/Pages/Posts/Index.vue --> <script setup> import { ref, watch } from 'vue' import { router } from '@inertiajs/vue3' import AppLayout from '@/Layouts/AppLayout.vue' import Pagination from '@/Components/Pagination.vue' import SearchFilter from '@/Components/SearchFilter.vue' const props = defineProps({ posts: Object, filters: Object, }) const search = ref(props.filters.search || '') const status = ref(props.filters.status || '') const category = ref(props.filters.category || '') // مراقبة تغييرات التصفية وتحديث URL watch([search, status, category], () => { router.get('/posts', { search: search.value, status: status.value, category: category.value, }, { preserveState: true, preserveScroll: true, replace: true, }) }, { debounce: 300 }) function deletePost(post) { if (confirm('هل أنت متأكد من أنك تريد حذف هذا المقال؟')) { router.delete(route('posts.destroy', post.id)) } } </script> <template> <AppLayout title="المقالات"> <div class="max-w-7xl mx-auto py-6"> <div class="flex justify-between items-center mb-6"> <h1 class="text-3xl font-bold">مقالات المدونة</h1> <Link :href="route('posts.create')" class="btn btn-primary" > إنشاء مقال </Link> </div> <!-- التصفيات --> <SearchFilter v-model:search="search" v-model:status="status" v-model:category="category" /> <!-- شبكة المقالات --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div v-for="post in posts.data" :key="post.id" class="card" > <Link :href="route('posts.show', post.id)"> <h3 class="font-bold text-xl mb-2">{{ post.title }}</h3> <p class="text-gray-600 mb-4">{{ post.excerpt }}</p> </Link> <div class="flex items-center justify-between"> <div class="flex items-center"> <img :src="post.author.avatar" :alt="post.author.name" class="w-8 h-8 rounded-full mr-2" > <span class="text-sm text-gray-500"> {{ post.author.name }} </span> </div> <div v-if="post.can.edit || post.can.delete" class="flex gap-2"> <Link v-if="post.can.edit" :href="route('posts.edit', post.id)" class="btn btn-sm" > تعديل </Link> <button v-if="post.can.delete" @click="deletePost(post)" class="btn btn-sm btn-danger" > حذف </button> </div> </div> </div> </div> <!-- الترقيم --> <Pagination :links="posts.links" class="mt-6" /> </div> </AppLayout> </template>
ملاحظة: استخدم خيارات preserveState وpreserveScroll عند التصفية للحفاظ على حالة الصفحة الحالية وموضع التمرير. استخدم replace: true لاستبدال إدخال السجل بدلاً من إضافة واحد جديد.

معالجة النماذج

توفر Inertia مساعد نموذج للتعامل مع عمليات إرسال النماذج:

<!-- resources/js/Pages/Posts/Create.vue --> <script setup> import { useForm } from '@inertiajs/vue3' import AppLayout from '@/Layouts/AppLayout.vue' import TextInput from '@/Components/TextInput.vue' import TextArea from '@/Components/TextArea.vue' import SelectInput from '@/Components/SelectInput.vue' const props = defineProps({ categories: Array, tags: Array, }) const form = useForm({ title: '', content: '', category_id: null, tags: [], featured_image: null, }) function submit() { form.post(route('posts.store'), { preserveScroll: true, onSuccess: () => form.reset(), onError: (errors) => { console.error('أخطاء التحقق:', errors) }, }) } function handleFileChange(event) { form.featured_image = event.target.files[0] } </script> <template> <AppLayout title="إنشاء مقال"> <div class="max-w-3xl mx-auto py-6"> <h1 class="text-3xl font-bold mb-6">إنشاء مقال جديد</h1> <form @submit.prevent="submit" class="space-y-6"> <!-- العنوان --> <TextInput v-model="form.title" label="العنوان" :error="form.errors.title" required /> <!-- الفئة --> <SelectInput v-model="form.category_id" label="الفئة" :options="categories" :error="form.errors.category_id" required /> <!-- المحتوى --> <TextArea v-model="form.content" label="المحتوى" :error="form.errors.content" rows="10" required /> <!-- الوسوم (اختيار متعدد) --> <div> <label class="block font-medium mb-2">الوسوم</label> <div class="flex flex-wrap gap-2"> <label v-for="tag in tags" :key="tag.id" class="inline-flex items-center" > <input type="checkbox" :value="tag.id" v-model="form.tags" class="rounded" > <span class="ml-2">{{ tag.name }}</span> </label> </div> <div v-if="form.errors.tags" class="text-red-600 text-sm mt-1"> {{ form.errors.tags }} </div> </div> <!-- الصورة المميزة --> <div> <label class="block font-medium mb-2">الصورة المميزة</label> <input type="file" @change="handleFileChange" accept="image/*" class="block w-full" > <div v-if="form.errors.featured_image" class="text-red-600 text-sm mt-1"> {{ form.errors.featured_image }} </div> <!-- شريط التقدم --> <progress v-if="form.progress" :value="form.progress.percentage" max="100" class="w-full mt-2" > {{ form.progress.percentage }}% </progress> </div> <!-- أزرار الإرسال --> <div class="flex gap-4"> <button type="submit" :disabled="form.processing" class="btn btn-primary" > <span v-if="form.processing">جارٍ الإنشاء...</span> <span v-else>إنشاء مقال</span> </button> <Link :href="route('posts.index')" class="btn" > إلغاء </Link> </div> </form> </div> </AppLayout> </template>
نصيحة: مساعد useForm() يتتبع حالة النموذج، وأخطاء التحقق، ويوفر خصائص مفيدة مثل processing، progress، وطرق مثل reset().

العرض من جانب الخادم (SSR)

قم بتمكين SSR لتحسين SEO وأداء تحميل الصفحة الأولي:

# تثبيت تبعيات SSR npm install @inertiajs/vue3-ssr @vue/server-renderer # إنشاء نقطة دخول SSR // resources/js/ssr.js import { createSSRApp, h } from 'vue' import { renderToString } from '@vue/server-renderer' import { createInertiaApp } from '@inertiajs/vue3' import createServer from '@inertiajs/vue3/server' import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m' 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) .use(ZiggyVue, { ...page.props.ziggy, location: new URL(page.props.ziggy.location), }) }, }) ) # تحديث 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', 'resources/js/ssr.js'], ssr: 'resources/js/ssr.js', refresh: true, }), vue({ template: { transformAssetUrls: { base: null, includeAbsolute: false, }, }, }), ], }) # بناء أصول SSR npm run build # بدء خادم SSR php artisan inertia:start-ssr
تحذير: يتطلب SSR خادم Node.js يعمل بجانب تطبيق PHP الخاص بك. تأكد من بدء خادم SSR باستخدام php artisan inertia:start-ssr في الإنتاج وتكوينه لإعادة التشغيل التلقائي عند بدء تشغيل الخادم.
تمرين 1: قم ببناء نظام إدارة المنتجات باستخدام Inertia.js. أنشئ صفحات لـ: إدراج المنتجات (مع البحث، والتصفية حسب الفئة، والترقيم)، وإنشاء منتج (مع تحميل الصورة)، وتعديل منتج، وعرض تفاصيل المنتج. قم بتضمين فحوصات التفويض (can.edit، can.delete) في البيانات المشتركة.
تمرين 2: قم بتنفيذ نظام عربة التسوق حيث يمكن للمستخدمين إضافة / إزالة العناصر، وتحديث الكميات، والمتابعة إلى الدفع. استخدم مساعد نموذج Inertia لنموذج الدفع مع التحقق. اعرض إجماليات العربة في الوقت الفعلي بدون تحديث الصفحة باستخدام preserve state.
تمرين 3: أنشئ لوحة معلومات مع بيانات في الوقت الفعلي تستطلع كل 30 ثانية للتحديثات. قم بتضمين المخططات (باستخدام مكتبة مثل Chart.js)، وبطاقات الإحصائيات، وموجز النشاط الأخير. استخدم router.reload() من Inertia مع خيار only لتحديث أجزاء محددة من الصفحة دون إعادة جلب كل شيء.

الملخص

في هذا الدرس، تعلمت:

  • ما هو Inertia.js ولماذا هو مفيد للتطبيقات المونوليثية الحديثة
  • كيفية تثبيت وتكوين Inertia مع Laravel و Vue 3
  • إنشاء وحدات التحكم التي تعيد استجابات Inertia مع البيانات
  • بناء مكونات Vue كصفحات Inertia
  • التعامل مع النماذج مع التحقق وتحميلات الملفات
  • تنفيذ العرض من جانب الخادم لتحسين SEO