إجراءات الخادم والتحولات

إجراءات الخادم (Server Actions) هي وظائف غير متزامنة يتم تنفيذها على الخادم. يمكن استدعاؤها في مكونات الخادم والعميل للتعامل مع إرسال النماذج وتحولات البيانات في تطبيقات Next.js.

🎥 شاهد: تعلم المزيد عن التحولات باستخدام إجراءات الخادم → YouTube (10 دقائق).

الاتفاقية

يمكن تعريف إجراء الخادم باستخدام توجيه React "use server". يمكنك وضع التوجيه في أعلى دالة async لتمييز الدالة كإجراء خادم، أو في أعلى ملف منفصل لتمييز جميع الصادرات من ذلك الملف كإجراءات خادم.

مكونات الخادم

يمكن لمكونات الخادم استخدام توجيه "use server" على مستوى الدالة المضمنة أو مستوى الوحدة. لتضمين إجراء خادم، أضف "use server" في أعلى جسم الدالة:

export default function Page() {
  // إجراء الخادم
  async function create() {
    'use server'
    // تحويل البيانات
  }

  return '...'
}
export default function Page() {
  // إجراء الخادم
  async function create() {
    'use server'
    // تحويل البيانات
  }

  return '...'
}

مكونات العميل

لاستدعاء دالة الخادم في مكون عميل، أنشئ ملفًا جديدًا وأضف توجيه "use server" في أعلى الملف. سيتم تمييز جميع الدوال المصدرة داخل الملف كدوال خادم يمكن إعادة استخدامها في كل من مكونات العميل والخادم:

'use server'

export async function create() {}
'use server'

export async function create() {}
'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Create</button>
}
'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Create</button>
}

تمرير الإجراءات كخصائص

يمكنك أيضًا تمرير إجراء خادم إلى مكون عميل كخاصية:

<ClientComponent updateItemAction={updateItem} />
'use client'

export default function ClientComponent({
  updateItemAction,
}: {
  updateItemAction: (formData: FormData) => void
}) {
  return <form action={updateItemAction}>{/* ... */}</form>
}
'use client'

export default function ClientComponent({ updateItemAction }) {
  return <form action={updateItemAction}>{/* ... */}</form>
}

عادةً، سيشير ملحق TypeScript لـ Next.js إلى updateItemAction في client-component.tsx لأنه دالة لا يمكن عادةً تسلسلها عبر حدود العميل والخادم. ومع ذلك، يُفترض أن الخصائص المسماة action أو المنتهية بـ Action تتلقى إجراءات خادم. هذا مجرد استدلال لأن ملحق TypeScript لا يعرف فعليًا ما إذا كان يتلقى إجراء خادم أو دالة عادية. سيظل التحقق من النوع أثناء التشغيل يضمن عدم تمرير دالة إلى مكون عميل عن طريق الخطأ.

السلوك

  • يمكن استدعاء إجراءات الخادم باستخدام سمة action في عنصر <form>.
    • تدعم مكونات الخادم التحسين التدريجي افتراضيًا، مما يعني أن النموذج سيتم إرساله حتى إذا لم يتم تحميل JavaScript بعد أو تم تعطيله.
    • في مكونات العميل، ستقوم النماذج التي تستدعي إجراءات الخادم بجدولة عمليات الإرسال إذا لم يتم تحميل JavaScript بعد، مع إعطاء الأولوية لترطيب العميل.
    • بعد الترطيب، لا يقوم المتصفح بالتحديث عند إرسال النموذج.
  • لا تقتصر إجراءات الخادم على <form> ويمكن استدعاؤها من معالج الأحداث، وuseEffect، ومكتبات الطرف الثالث، وعناصر النموذج الأخرى مثل <button>.
  • تندمج إجراءات الخادم مع بنية التخزين المؤقت وإعادة التحقق في Next.js. عند استدعاء إجراء، يمكن لـ Next.js إرجاع واجهة المستخدم المحدثة والبيانات الجديدة في جولة خادم واحدة.
  • خلف الكواليس، تستخدم الإجراءات طريقة POST، ويمكن استدعاؤها فقط باستخدام طريقة HTTP هذه.
  • يجب أن تكون وسيطات وقيمة إرجاع إجراءات الخادم قابلة للتسلسل بواسطة React. راجع وثائق React للحصول على قائمة بوسيطات وقيم الإرجاع القابلة للتسلسل.
  • إجراءات الخادم هي دوال. هذا يعني أنه يمكن إعادة استخدامها في أي مكان في تطبيقك.
  • ترث إجراءات الخادم وقت التشغيل من الصفحة أو التخطيط الذي تستخدمه.
  • ترث إجراءات الخادم تكوين مقطع المسار (Route Segment Config) من الصفحة أو التخطيط الذي تستخدمه، بما في ذلك الحقول مثل maxDuration.

أمثلة

معالجات الأحداث

بينما من الشائع استخدام إجراءات الخادم داخل عناصر <form>، يمكن أيضًا استدعاؤها باستخدام معالجات الأحداث مثل onClick. على سبيل المثال، لزيادة عدد الإعجابات:

'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}
'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}

يمكنك أيضًا إضافة معالجات الأحداث إلى عناصر النموذج، على سبيل المثال، لحفظ حقل النموذج onChange:

app/ui/edit-post.tsx
'use client'

import { publishPost, saveDraft } from './actions'

export default function EditPost() {
  return (
    <form action={publishPost}>
      <textarea
        name="content"
        onChange={async (e) => {
          await saveDraft(e.target.value)
        }}
      />
      <button type="submit">Publish</button>
    </form>
  )
}

في حالات مثل هذه، حيث قد يتم تشغيل أحداث متعددة بسرعة، نوصي باستخدام إزالة الارتداد (debouncing) لمنع استدعاءات إجراءات الخادم غير الضرورية.

useEffect

يمكنك استخدام خطاف React useEffect لاستدعاء إجراء خادم عند تحميل المكون أو تغيير التبعية. هذا مفيد للتحولات التي تعتمد على أحداث عالمية أو تحتاج إلى تشغيلها تلقائيًا. على سبيل المثال، onKeyDown لاختصارات التطبيق، أو خطاف مراقبة التقاطع (intersection observer) للتمرير اللانهائي، أو عند تحميل المكون لتحديث عدد المشاهدات:

'use client'

import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
  const [isPending, startTransition] = useTransition()

  useEffect(() => {
    startTransition(async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    })
  }, [])

  // يمكنك استخدام `isPending` لإعطاء المستخدمين ملاحظات
  return <p>Total Views: {views}</p>
}
'use client'

import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'

export default function ViewCount({ initialViews }) {
  const [views, setViews] = useState(initialViews)
  const [isPending, startTransition] = useTransition()

  useEffect(() => {
    starTransition(async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    })
  }, [])

  // يمكنك استخدام `isPending` لإعطاء المستخدمين ملاحظات
  return <p>Total Views: {views}</p>
}

تذكر مراعاة السلوك والتحذيرات لـ useEffect.

معالجة الأخطاء

عند حدوث خطأ، سيتم التقاطه بواسطة أقرب error.js أو حد <Suspense> على العميل. راجع معالجة الأخطاء لمزيد من المعلومات.

جيد أن تعرف:

  • بالإضافة إلى إلقاء الخطأ، يمكنك أيضًا إرجاع كائن ليتم معالجته بواسطة useActionState.

إعادة التحقق من البيانات

يمكنك إعادة التحقق من ذاكرة التخزين المؤقت لـ Next.js داخل إجراءات الخادم الخاصة بك باستخدام واجهة برمجة التطبيقات revalidatePath:

'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidatePath('/posts')
}
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidatePath('/posts')
}

أو إبطال جلب بيانات معين باستخدام علامة ذاكرة تخزين مؤقت مع revalidateTag:

'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts')
}
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts')
}

إعادة التوجيه

إذا كنت ترغب في إعادة توجيه المستخدم إلى مسار مختلف بعد اكتمال إجراء الخادم، يمكنك استخدام واجهة برمجة التطبيقات redirect. يجب استدعاء redirect خارج كتلة try/catch:

'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function createPost(id: string) {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts') // تحديث المشاركات المخزنة مؤقتًا
  redirect(`/post/${id}`) // الانتقال إلى صفحة المشاركة الجديدة
}
'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function createPost(id) {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts') // تحديث المشاركات المخزنة مؤقتًا
  redirect(`/post/${id}`) // الانتقال إلى صفحة المشاركة الجديدة
}

ملفات تعريف الارتباط (Cookies)

يمكنك الحصول على، تعيين، وحذف ملفات تعريف الارتباط داخل إجراء خادم باستخدام واجهة برمجة التطبيقات cookies:

'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  const cookieStore = await cookies()

  // الحصول على ملف تعريف الارتباط
  cookieStore.get('name')?.value

  // تعيين ملف تعريف الارتباط
  cookieStore.set('name', 'Delba')

  // حذف ملف تعريف الارتباط
  cookieStore.delete('name')
}
'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  // الحصول على ملف تعريف الارتباط
  const cookieStore = await cookies()

  // الحصول على ملف تعريف الارتباط
  cookieStore.get('name')?.value

  // تعيين ملف تعريف الارتباط
  cookieStore.set('name', 'Delba')

  // حذف ملف تعريف الارتباط
  cookieStore.delete('name')
}

راجع أمثلة إضافية لحذف ملفات تعريف الارتباط من إجراءات الخادم.

الأمان

افتراضيًا، عند إنشاء إجراء خادم وتصديره، فإنه ينشئ نقطة نهاية HTTP عامة ويجب التعامل معه بنفس افتراضات الأمان وفحوصات التفويض. هذا يعني، حتى إذا لم يتم استيراد إجراء خادم أو دالة مساعدة في مكان آخر في الكود الخاص بك، فإنه لا يزال يمكن الوصول إليه بشكل عام.

لتحسين الأمان، يحتوي Next.js على الميزات المضمنة التالية:

  • معرفات الإجراءات الآمنة: ينشئ Next.js معرفات مشفرة وغير حتمية للسماح للعميل بالإشارة إلى إجراء الخادم واستدعائه. يتم إعادة حساب هذه المعرفات بشكل دوري بين عمليات البناء لتعزيز الأمان.
  • إزالة الكود الميت: يتم إزالة إجراءات الخادم غير المستخدمة (المشار إليها بواسطة معرفاتها) من حزمة العميل لتجنب الوصول العام من قبل طرف ثالث.

جيد أن تعرف:

يتم إنشاء المعرفات أثناء التجميع ويتم تخزينها مؤقتًا لمدة أقصاها 14 يومًا. سيتم إعادة توليدها عند بدء بناء جديد أو عند إبطال ذاكرة التخزين المؤقت للبناء. هذا التحسين الأمني يقلل من المخاطر في الحالات التي يكون فيها طبقة المصادقة مفقودة. ومع ذلك، يجب أن تتعامل مع إجراءات الخادم مثل نقاط نهاية HTTP العامة.

// app/actions.js
'use server'

// هذا الإجراء **مستخدم** في تطبيقنا، لذا سيقوم Next.js
// بإنشاء معرف آمن للسماح للعميل بالإشارة
// واستدعاء إجراء الخادم.
export async function updateUserAction(formData) {}

// هذا الإجراء **غير مستخدم** في تطبيقنا، لذا سيقوم Next.js
// تلقائيًا بإزالة هذا الكود أثناء `next build`
// ولن ينشئ نقطة نهاية عامة.
export async function deleteUserAction(formData) {}

المصادقة والتفويض

يجب عليك التأكد من أن المستخدم مفوض لتنفيذ الإجراء. على سبيل المثال:

app/actions.ts
'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('You must be signed in to perform this action')
  }

  // ...
}

الإغلاق والتشفير

يؤدي تعريف إجراء خادم داخل مكون إلى إنشاء إغلاق (closure) حيث يكون للإجراء حق الوصول إلى نطاق الدالة الخارجية. على سبيل المثال، لدى إجراء publish حق الوصول إلى متغير publishVersion:

export default async function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }

  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  );
}
export default async function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }

  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  );
}

الإغلاق مفيد عندما تحتاج إلى التقاط لقطة للبيانات (مثل publishVersion) في وقت العرض بحيث يمكن استخدامها لاحقًا عند استدعاء الإجراء.

ومع ذلك، لكي يحدث هذا، يتم إرسال المتغيرات الملتقطة إلى العميل والعودة إلى الخادم عند استدعاء الإجراء. لمنع تعرض البيانات الحساسة للعميل، يقوم Next.js تلقائيًا بتشفير المتغيرات المغلقة. يتم إنشاء مفتاح خاص جديد لكل إجراء في كل مرة يتم فيها بناء تطبيق Next.js. هذا يعني أنه يمكن استدعاء الإجراءات فقط لبناء محدد.

جيد أن تعرف: لا نوصي بالاعتماد على التشفير وحده لمنع تعرض القيم الحساسة على العميل. بدلاً من ذلك، يجب عليك استخدام واجهات برمجة تطبيقات تلويث React (React taint APIs) لمنع إرسال بيانات معينة إلى العميل بشكل استباقي.

تجاوز مفاتيح التشفير (متقدم)

عند استضافة تطبيق Next.js الخاص بك على خوادم متعددة، قد ينتهي كل مثيل خادم بمفتاح تشفير مختلف، مما يؤدي إلى احتمالية حدوث تناقضات.

لتخفيف هذه المشكلة، يمكنك تجاوز مفتاح التشفير باستخدام متغير البيئة process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY. تحديد هذا المتغير يضمن أن مفاتيح التشفير الخاصة بك تظل ثابتة عبر عمليات البناء، وأن جميع مثيلات الخادم تستخدم نفس المفتاح. يجب أن يكون هذا المتغير مشفّرًا باستخدام AES-GCM.

هذه حالة استخدام متقدمة حيث يكون سلوك التشفير المتسق عبر عمليات النشر المتعددة أمرًا بالغ الأهمية لتطبيقك. يجب أن تفكر في ممارسات الأمان القياسية مثل تدوير المفاتيح والتوقيع.

معلومة مفيدة: التطبيقات التي يتم نشرها على Vercel تتعامل مع هذا تلقائيًا.

الأصول المسموح بها (متقدم)

نظرًا لأنه يمكن استدعاء إجراءات الخادم (Server Actions) في عنصر <form>، فإن هذا يعرضها لهجمات CSRF.

خلف الكواليس، تستخدم إجراءات الخادم طريقة POST، وهذه الطريقة فقط مسموح بها لاستدعائها. هذا يمنع معظم نقاط الضعف في CSRF في المتصفحات الحديثة، خاصة مع كون ملفات تعريف الارتباط SameSite هي الافتراضية.

كحماية إضافية، تقارن إجراءات الخادم في Next.js أيضًا رأس Origin مع رأس Host (أو X-Forwarded-Host). إذا لم تتطابق هذه الرؤوس، سيتم إلغاء الطلب. بعبارة أخرى، يمكن استدعاء إجراءات الخادم فقط على نفس المضيف الذي يستضيف الصفحة.

بالنسبة للتطبيقات الكبيرة التي تستخدم خوادم وكيلة عكسية (reverse proxies) أو بنى خلفية متعددة الطبقات (حيث يختلف خادم API عن نطاق الإنتاج)، يُوصى باستخدام خيار التكوين serverActions.allowedOrigins لتحديد قائمة بالأصول الآمنة. يقبل هذا الخيار مصفوفة من السلاسل النصية.

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

تعرف على المزيد حول الأمان وإجراءات الخادم.

موارد إضافية

لمزيد من المعلومات، راجع الوثائق التالية لـ React: