فرم را می‌فرستید، صفحه کامل بارگذاری مجدد می‌شود، اسکرول به بالا می‌پرد و کاربر دوباره باید پایین بیاید. AJAX یعنی همین ارتباط با سرور را پشت صحنه انجام دهید و فقط بخش لازم صفحه را عوض کنید. سال‌ها برای این کار سراغ jQuery می‌رفتیم؛ ولی مرورگرها حالا تابع fetch را دارند و دیگر نیازی به کتابخانه نیست.

اگر پایه‌ی async/await برایتان تازه است، اول جاوااسکریپت ضروری برای برنامه‌نویس وردپرس و لاراول را بخوانید.

ساده‌ترین درخواست: GET#

JavaScript
async function loadPosts() {
  const res = await fetch('/api/posts');
  if (!res.ok) {
    throw new Error(`پاسخ ناموفق: ${res.status}`);
  }
  const posts = await res.json();
  return posts;
}

loadPosts()
  .then((posts) => console.log(posts))
  .catch((err) => console.error(err));

دو نکته در همین مثال کوتاه است. اول، fetch یک Promise برمی‌گرداند و res.json() هم Promise است، پس هر دو await می‌خواهند. دوم، پاسخ ۴۰۴ یا ۵۰۰ خودکار خطا نمی‌شود؛ بررسی res.ok با شماست.

ارسال JSON با POST#

JavaScript
async function createComment(postId, text) {
  const res = await fetch('/api/comments', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
    body: JSON.stringify({ post_id: postId, text }),
  });

  if (!res.ok) {
    const problem = await res.json().catch(() => ({}));
    throw new Error(problem.message ?? `خطا: ${res.status}`);
  }
  return res.json();
}

بدنه باید رشته باشد، پس JSON.stringify لازم است، و هدر Content-Type به سرور می‌گوید با چه قالبی طرف است. برای ارسال فرم و فایل از FormData استفاده کنید و هدر Content-Type را خودتان تنظیم نکنید؛ مرورگر مرز (boundary) را اضافه می‌کند:

JavaScript
const form = document.querySelector('#contact');

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const data = new FormData(form);

  const res = await fetch(form.action, { method: 'POST', body: data });
  const result = await res.json();
  console.log(result);
});

مدیریت خطا و لغو درخواست#

سه حالت را جدا در نظر بگیرید: قطع شبکه (استثنا)، پاسخ ناموفق (!res.ok) و داده‌ی خراب (خطا در json()). برای درخواست‌های کند، لغو با AbortController مفید است:

JavaScript
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);

try {
  const res = await fetch('/api/slow', { signal: controller.signal });
  console.log(await res.json());
} catch (err) {
  if (err.name === 'AbortError') {
    console.warn('درخواست لغو شد');
  } else {
    console.error(err);
  }
} finally {
  clearTimeout(timer);
}

همچنین در رابط کاربری حالت «در حال بارگذاری» را نشان دهید و دکمه را تا پایان درخواست غیرفعال کنید تا کاربر دوبار نفرستد.

اتصال به وردپرس#

در وردپرس دو راه رایج دارید: REST API یا admin-ajax.php. روش امروزی، REST است. اول در PHP مسیر را با بررسی دسترسی ثبت می‌کنید:

PHP
<?php
add_action('rest_api_init', function () {
    register_rest_route('demo/v1', '/note', [
        'methods'             => 'POST',
        'callback'            => 'demo_save_note',
        'permission_callback' => fn () => current_user_can('edit_posts'),
        'args'                => [
            'text' => [
                'required'          => true,
                'sanitize_callback' => 'sanitize_text_field',
            ],
        ],
    ]);
});

function demo_save_note(WP_REST_Request $request): WP_REST_Response
{
    $text = $request->get_param('text');
    // ذخیره‌ی امن...
    return new WP_REST_Response(['saved' => true, 'text' => esc_html($text)], 200);
}

بعد اسکریپت را ثبت کنید و آدرس و nonce را با wp_localize_script بدهید:

PHP
<?php
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script('demo-note', plugins_url('note.js', __FILE__), [], '1.0', true);
    wp_localize_script('demo-note', 'demoNote', [
        'url'   => esc_url_raw(rest_url('demo/v1/note')),
        'nonce' => wp_create_nonce('wp_rest'),
    ]);
});

و در جاوااسکریپت:

JavaScript
async function saveNote(text) {
  const res = await fetch(demoNote.url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-WP-Nonce': demoNote.nonce,
    },
    body: JSON.stringify({ text }),
  });
  return res.json();
}

هسته‌ی وردپرس هدر X-WP-Nonce را بررسی می‌کند و بدون آن، کاربر واردشده را ناشناس می‌شناسد. permission_callback را هرگز خالی یا __return_true نگذارید مگر اینکه مسیر واقعاً عمومی باشد. ساختار کلی افزونه در ساخت افزونه‌ی وردپرس از صفر آمده است.

اتصال به لاراول#

لاراول توکن CSRF را برای درخواست‌های POST در مسیرهای وب می‌خواهد. توکن را در head صفحه بگذارید:

HTML
<meta name="csrf-token" content="{{ csrf_token() }}">

و در درخواست بفرستید:

JavaScript
const token = document.querySelector('meta[name="csrf-token"]').content;

const res = await fetch('/notes', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'X-CSRF-TOKEN': token,
  },
  body: JSON.stringify({ text: 'سلام' }),
});

هدر Accept: application/json باعث می‌شود لاراول خطای اعتبارسنجی را به‌شکل JSON (کد ۴۲۲) بدهد، نه ریدایرکت. توکن را در body هم می‌توانید با فیلد _token بفرستید.

چند توصیه‌ی پایانی#

  • پاسخ سرور را با textContent در صفحه بگذارید، نه innerHTML.
  • اعتبارسنجی و دسترسی را در سرور انجام دهید؛ کد مرورگر قابل‌دستکاری است.
  • URL ساختن با داده‌ی کاربر را با encodeURIComponent یا URLSearchParams انجام دهید.
  • در صفحه‌ای که چند درخواست دارد، حالت خطا را برای کاربر قابل‌فهم نشان دهید.

برای فهم بهتر طراحی مسیرها و کدهای وضعیت، REST API چیست را هم ببینید.

الگوی تمیز: یک تابع کمکی#

اگر در چند جا درخواست می‌فرستید، تکرار نکنید. یک تابع کوچک بنویسید که هدرها، بررسی خطا و تبدیل JSON را یک‌جا انجام دهد:

JavaScript
async function api(url, { method = 'GET', body, headers = {} } = {}) {
  const res = await fetch(url, {
    method,
    headers: {
      'Accept': 'application/json',
      ...(body ? { 'Content-Type': 'application/json' } : {}),
      ...headers,
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  const data = await res.json().catch(() => null);
  if (!res.ok) {
    throw new Error(data?.message ?? `خطا: ${res.status}`);
  }
  return data;
}

// استفاده
const posts = await api('/api/posts');
await api('/api/comments', { method: 'POST', body: { text: 'سلام' } });

با این کار منطق خطا یک‌جا نگه داشته می‌شود و اگر بعداً بخواهید توکن یا هدر تازه‌ای اضافه کنید، فقط یک تابع را عوض می‌کنید.

ارسال پارامتر در GET#

برای پارامترهای جست‌وجو، رشته را با دست نچسبانید. URLSearchParams مقدارها را درست کدگذاری می‌کند و جلوی خطاهای ظریف را می‌گیرد، مخصوصاً وقتی کاربر فارسی یا نویسه‌ی خاص تایپ کرده است:

JavaScript
const params = new URLSearchParams({ q: 'دوره‌ی PHP', page: 2 });
const res = await fetch(`/api/search?${params}`);

نکته‌ی امنیتی درباره‌ی کوکی‌ها#

درخواست fetch به همان مبدأ، کوکی را به‌صورت پیش‌فرض می‌فرستد. برای مبدأ دیگر باید credentials: 'include' بگذارید و سرور هم صریحاً اجازه بدهد؛ این کار را فقط وقتی لازم است انجام دهید، چون درخواست‌های مبدأ دیگر همراه کوکی، مهاجم را به CSRF نزدیک می‌کنند. امنیت کد PHP توضیح می‌دهد چرا توکن اهمیت دارد.

خلاصه#

  • fetch در مرورگر هست و نیازی به jQuery نیست.
  • همیشه res.ok را بسنجید؛ خطای HTTP استثنا نمی‌شود.
  • JSON را با JSON.stringify و هدر Content-Type بفرستید؛ FormData را بدون تنظیم Content-Type بفرستید.
  • در وردپرس از REST API با permission_callback و هدر X-WP-Nonce استفاده کنید.
  • در لاراول توکن CSRF را در هدر X-CSRF-TOKEN بفرستید.
  • خروجی را با textContent نمایش دهید و اعتبارسنجی را به سرور بسپارید.