(API)ربط الواجهة الأمامية بالـ API
كيف تتحدث صفحات JavaScript وتطبيقات React إلى واجهتك الخلفية؟ ⏱ نحو 40 دقيقة.
- تميّز بين صفحات يبنيها الخادم وواجهات تطلب
APIبـfetch. - تربط صفحة
HTML/JSبواجهةREST: قراءة وإضافة وحذف بـfetchوJSON. - تعالج رموز الحالة والأخطاء في الواجهة الأمامية.
- تشرح
CORSوالطلب التمهيدي، وتضبط ترويساته فيPHPلأصول محددة.
١نوعان من الصفحات
حتى الآن كان الخادم يبني صفحة HTML كاملة مع كل طلب. والأسلوب الآخر المنتشر اليوم: الخادم يرسل صفحة ثابتة فيها JavaScript، ثم يطلب JavaScript البيانات من الـ API بـ fetch، ويحدّث الصفحة دون إعادة تحميلها. هكذا تعمل تطبيقات React (مقرر 242) وتطبيقات الجوال (251).
| الخادم يبني الصفحة (الدروس السابقة) | الواجهة تطلب API | |
|---|---|---|
| ماذا يرسل الخادم؟ | HTML جاهزًا | JSON فقط |
| من يبني HTML؟ | PHP على الخادم | JavaScript في المتصفح |
| بعد إضافة عنصر | إعادة تحميل الصفحة كاملة | تحديث جزء من الصفحة فقط |
| مناسب لـ | المواقع والمحتوى ومحركات البحث | لوحات التحكم والتطبيقات التفاعلية |
ولاحظ أن الـ API نفسها من الدرس السابق تخدم الصفحة، وتخدم تطبيق الجوال، وأي عميل آخر. فالواجهة الخلفية الواحدة تخدم واجهات أمامية كثيرة.
٢التطبيق كاملًا: صفحة + API
ملف index.html فيه HTML و JavaScript فقط (من مقرر 131)، وملف api/users.php هو واجهة الدرس 22 دون أي تغيير. شغّل المختبر وجرّب الإضافة والحذف، ثم افتح «📜 السجل»: سترى كل طلب أرسله fetch إلى الخادم:
٣ثلاث قواعد لـ fetch مع API
- fetch لا ترمي خطأ عند 404 أو 422. ترمي فقط إن انقطع الاتصال. فافحص
res.ok(صحيح للرموز 200–299) أوres.statusبنفسك. - أرسل JSON بترويسته:
Content-Type: application/jsonوJSON.stringify(data)، وإلا لن يفهم الخادم الجسم. - الرد 204 بلا جسم: لا تستدعِ
res.json()عليه فيفشل.
async function api(method, url, data) { const res = await fetch(url, { method, headers: data ? { 'Content-Type': 'application/json' } : {}, body: data ? JSON.stringify(data) : undefined, }); const body = res.status === 204 ? null : await res.json(); if (!res.ok) throw new Error(body?.error || 'HTTP ' + res.status); return body;} // الاستخدامtry { const user = await api('POST', 'api/users.php', { name: 'ريم', email: 'r@tvtc.sa' });} catch (e) { alert(e.message); // رسالة الخادم نفسها: «البريد مسجّل من قبل»}
٤CORS: حين تكون الواجهة على عنوان آخر
في المثال السابق الصفحة والـ API على العنوان نفسه. لكن أثناء تطوير React تعمل الواجهة على http://localhost:5173 والـ API على http://localhost: «أصلان» Origins مختلفان. والمتصفح يمنع لحماية المستخدمين أن تقرأ صفحة من أصل ردود أصل آخر، إلا إذا سمح الخادم صراحة. هذه السياسة اسمها CORS (مشاركة الموارد بين الأصول).
- المتصفح يرسل مع الطلب الترويسة
Origin: http://localhost:5173. - إن كان الطلب «غير بسيط» (مثل POST بجسم JSON، أو DELETE) يرسل أولًا طلبًا تمهيديًّا OPTIONS يسأل: هل تسمح؟
- الخادم يجيب بترويسات
Access-Control-Allow-…. - إن وافقت، يرسل المتصفح الطلب الحقيقي ويعطي الرد للصفحة؛ وإلا يحجبه ويظهر خطأ CORS في Console.
<?php$allowed = ['http://localhost:5173', 'https://app.tvtc-demo.sa'];$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (in_array($origin, $allowed, true)) { header("Access-Control-Allow-Origin: $origin"); header('Access-Control-Allow-Methods: GET, POST, DELETE'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); header('Vary: Origin');}if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); // الطلب التمهيدي: الترويسات تكفي exit;}header('Content-Type: application/json');echo '{"pong": true}';
الأصل المسموح أخذ ترويسات Access-Control، والأصل الغريب لم يأخذها، فسيحجب متصفح ضحيته الرد. لاحظ أن الخادم ردّ في الحالتين: CORS قيد في المتصفح، لا حماية للخادم من الطلبات المباشرة.
توقّع وتحقّق
fetch إلى API ردّت 404. ماذا يحدث؟
res.ok.الطلب التمهيدي في CORS يستخدم الطريقة:
Preflight.من يطبّق سياسة CORS؟
الترويسة التي يسمح بها الخادم لأصل معيّن:
الخلاصة
- الخادم يرسل
JSON، وJavaScriptيبني الصفحة ويحدّثها بلا إعادة تحميل. fetch(url, {method, headers, body: JSON.stringify(data)})ثمres.json().fetchلا ترمي عند4xx: افحصres.ok، ولا تقرأJSONمن 204.CORS: أصل مختلف يحتاج إذن الخادم بترويساتAccess-Control-Allow-*، وOPTIONSللطلب التمهيدي.- حدّد الأصول المسموحة بقائمة، ولا تعتمد على
CORSحمايةً للخادم.
تحدٍّ برمجي: افتح الواجهة لتطبيق React
متقدمفريق الواجهة الأمامية يبني تطبيق React على عنوانين، ويحتاج أن يقرأ api/list.php. أضف دعم CORS للأصلين فقط.
- الأصلان المسموحان:
http://localhost:5173وhttps://lab.tvtc-demo.sa. - للأصل المسموح: الترويسة
Access-Control-Allow-Originبقيمة الأصل نفسه (لا *)، ومعهاAllow-Methodsفيها PUT، وAllow-HeadersفيهاContent-Type. - الطلب OPTIONS ← 204 بلا جسم.
- أصل غير مسموح ← لا ترويسة
Access-Control-Allow-Originإطلاقًا (لكن الرد نفسه يبقى 200).
👀 اعرض الاستجابة المطلوبة
OPTIONS /api/list.phpGET /api/list.phpGET /api/list.phpGET /api/list.php
- الكود يعمل بلا أخطاء أو تحذيرات
- OPTIONS ← 204
- OPTIONS ←
Allow-Originللأصل نفسه - OPTIONS ←
Allow-Methodsفيها PUT - OPTIONS ←
Allow-HeadersفيهاContent-Type - GET من
lab.tvtc-demo.sa← مسموح - GET ← البيانات كما هي
- أصل غريب ← الرد 200
- أصل غريب ← بلا ترويسة
Access-Control-Allow-Origin - لا تستخدم * (تحقق بقائمة)
- لا
Allow-Origin: *
💡 تلميح 1
$_SERVER['HTTP_ORIGIN'].💡 تلميح 2
http_response_code(204); exit;حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
<?phprequire __DIR__ . '/../db.php';header('Content-Type: application/json; charset=utf-8'); $allowed = ['http://localhost:5173', 'https://lab.tvtc-demo.sa'];$origin = $_SERVER['HTTP_ORIGIN'] ?? '';if (in_array($origin, $allowed, true)) { header("Access-Control-Allow-Origin: $origin"); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); header('Vary: Origin');}if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit;} $rows = $pdo->query('SELECT id, name FROM users ORDER BY id LIMIT 3')->fetchAll();echo json_encode($rows, JSON_UNESCAPED_UNICODE);
<?php$pdo = new PDO('sqlite:' . __DIR__ . '/app.sqlite');// $pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'root', '');$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);