جلب البيانات في Next.js
يوفر Next.js إمكانيات قوية لجلب البيانات تعمل بسلاسة مع مكونات الخادم. يغطي هذا الدرس واجهة برمجة التطبيقات الموسعة fetch، واستراتيجيات التخزين المؤقت، وتقنيات إعادة التحقق، والأنماط لتحميل البيانات بكفاءة.
واجهة برمجة التطبيقات الموسعة fetch()
يوسع Next.js واجهة برمجة تطبيقات الويب الأصلية fetch() لإضافة إزالة تكرار الطلبات التلقائية والتخزين المؤقت وإعادة التحقق. يتم تخزين جميع طلبات fetch تلقائياً في ذاكرة التخزين المؤقت على الخادم.
الابتكار الرئيسي: في Next.js 13+، يمكنك جلب البيانات مباشرة في مكونات الخادم دون إنشاء مسارات API منفصلة. هذا يبسط بنيتك ويحسن الأداء.
جلب البيانات الأساسي
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
if (!res.ok) {
throw new Error('فشل جلب المنشورات');
}
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>منشورات المدونة</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
);
}
أفضل ممارسة: أنشئ دوال async منفصلة لمنطق جلب البيانات الخاص بك. هذا يبقي مكوناتك نظيفة ويجعل الدوال قابلة لإعادة الاستخدام في جميع أنحاء التطبيق.
استراتيجيات التخزين المؤقت
يقوم Next.js تلقائياً بتخزين طلبات fetch مؤقتاً على الخادم. يمكنك التحكم في سلوك التخزين المؤقت باستخدام خيار cache:
1. فرض التخزين المؤقت (افتراضي)
يخزن الاستجابة مؤقتاً حتى يتم إبطالها يدوياً:
// هذا هو السلوك الافتراضي
fetch('https://api.example.com/posts', { cache: 'force-cache' });
// يعادل:
fetch('https://api.example.com/posts');
2. عدم التخزين (ديناميكي)
جلب بيانات جديدة في كل طلب:
// app/dashboard/page.tsx
async function getRealtimeData() {
const res = await fetch('https://api.example.com/realtime', {
cache: 'no-store'
});
return res.json();
}
export default async function DashboardPage() {
const data = await getRealtimeData();
return (
<div>
<h1>لوحة التحكم المباشرة</h1>
<p>آخر تحديث: {data.timestamp}</p>
<p>المستخدمون النشطون: {data.activeUsers}</p>
</div>
);
}
تأثير الأداء: استخدام cache: 'no-store' يعني أن الصفحة ستعرض ديناميكياً لكل طلب، والذي يمكن أن يكون أبطأ من العرض الثابت. استخدم هذا فقط للبيانات الديناميكية حقاً.
استراتيجيات إعادة التحقق
إعادة التحقق تسمح لك بمسح البيانات المخزنة مؤقتاً وإعادة جلبها في فترات محددة أو عند الطلب.
إعادة التحقق المستندة إلى الوقت
إعادة التحقق التلقائي من البيانات بعد فترة زمنية محددة:
// إعادة التحقق كل 60 ثانية
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>المنشورات (يتم التحديث كل 60 ثانية)</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
كيف يعمل: الطلب الأول يخزن البيانات مؤقتاً. الطلبات اللاحقة خلال 60 ثانية تخدم النسخة المخزنة. بعد 60 ثانية، يطلق الطلب التالي إعادة التحقق في الخلفية أثناء خدمة ذاكرة التخزين المؤقت القديمة. بمجرد اكتمال إعادة التحقق، تتحدث ذاكرة التخزين المؤقت.
تكوين قسم المسار
يمكنك تعيين سلوك إعادة التحقق الافتراضي لقسم مسار كامل:
// app/blog/page.tsx
export const revalidate = 3600; // إعادة التحقق كل ساعة
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
<h1>المدونة</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
}
إعادة التحقق عند الطلب
إعادة التحقق يدوياً من مسارات معينة أو علامات ذاكرة التخزين المؤقت باستخدام revalidatePath() أو revalidateTag():
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
// التحقق من صحة الرمز السري
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ message: 'رمز سري غير صالح' }, { status: 401 });
}
// إعادة التحقق من مسار معين
revalidatePath('/posts');
// أو إعادة التحقق بواسطة علامة التخزين المؤقت
revalidateTag('posts');
return Response.json({ revalidated: true, now: Date.now() });
}
استخدام علامات التخزين المؤقت في طلبات fetch الخاصة بك:
// ضع علامات على طلبات fetch الخاصة بك
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
});
return res.json();
}
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { tags: ['posts', `post-${id}`] }
});
return res.json();
}
// إعادة التحقق من جميع الطلبات الموسومة بـ 'posts'
// عن طريق استدعاء: POST /api/revalidate?secret=xxx
حالة الاستخدام: إعادة التحقق عند الطلب مثالية لأنظمة إدارة المحتوى. عندما ينشر محرر منشوراً جديداً، قم بتشغيل إعادة التحقق عبر webhook لتحديث موقعك فوراً دون انتظار إعادة التحقق المستندة إلى الوقت.
جلب البيانات المتوازي
جلب مصادر بيانات متعددة بشكل متوازٍ لتقليل وقت التحميل:
// app/dashboard/page.tsx
async function getUser() {
const res = await fetch('https://api.example.com/user');
return res.json();
}
async function getStats() {
const res = await fetch('https://api.example.com/stats');
return res.json();
}
async function getNotifications() {
const res = await fetch('https://api.example.com/notifications');
return res.json();
}
export default async function DashboardPage() {
// ❌ تسلسلي - بطيء (3 ثوانٍ إجمالاً إذا استغرق كل منها ثانية واحدة)
// const user = await getUser();
// const stats = await getStats();
// const notifications = await getNotifications();
// ✅ متوازٍ - سريع (ثانية واحدة إجمالاً)
const [user, stats, notifications] = await Promise.all([
getUser(),
getStats(),
getNotifications()
]);
return (
<div>
<h1>مرحباً، {user.name}!</h1>
<div>المشاهدات: {stats.views}</div>
<div>الإشعارات: {notifications.length}</div>
</div>
);
}
تأثير الأداء: استخدام Promise.all() يمكن أن يقلل وقت تحميل صفحتك بشكل كبير. إذا كان لديك 3 طلبات يستغرق كل منها ثانية واحدة، فإن الجلب التسلسلي يستغرق 3 ثوانٍ، بينما الجلب المتوازي يستغرق ثانية واحدة فقط.
جلب البيانات التسلسلي
في بعض الأحيان تحتاج إلى جلب البيانات بشكل تسلسلي لأن طلباً واحداً يعتمد على آخر:
// app/user/[id]/page.tsx
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
async function getUserPosts(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}/posts`);
return res.json();
}
export default async function UserPage({ params }: { params: { id: string } }) {
// أولاً جلب المستخدم
const user = await getUser(params.id);
// ثم جلب منشوراتهم باستخدام معرّفهم
const posts = await getUserPosts(user.id);
return (
<div>
<h1>ملف تعريف {user.name}</h1>
<p>السيرة الذاتية: {user.bio}</p>
<h2>المنشورات</h2>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
التحميل المسبق للبيانات
استخدم نمط التحميل المسبق لبدء جلب البيانات قبل الحاجة إليها:
// lib/data.ts
import { cache } from 'react';
export const getUser = cache(async (id: string) => {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
});
// إنشاء دالة تحميل مسبق
export const preloadUser = (id: string) => {
void getUser(id); // بدء الجلب لكن لا تنتظر
};
// app/user/[id]/layout.tsx
import { preloadUser, getUser } from '@/lib/data';
export default async function UserLayout({
children,
params
}: {
children: React.ReactNode;
params: { id: string };
}) {
// بدء تحميل بيانات المستخدم
preloadUser(params.id);
return (
<div>
<UserNav id={params.id} />
{children}
</div>
);
}
// app/user/[id]/UserNav.tsx
import { getUser } from '@/lib/data';
export default async function UserNav({ id }: { id: string }) {
// هذا سيستخدم البيانات المحملة مسبقاً
const user = await getUser(id);
return (
<nav>
<img src={user.avatar} alt={user.name} />
<span>{user.name}</span>
</nav>
);
}
React cache(): دالة cache() من React تزيل تكرار الطلبات ضمن ممر عرض واحد. الاستدعاءات المتعددة لـ getUser(id) بنفس المعرّف ستجلب مرة واحدة فقط.
معالجة الأخطاء
معالجة الأخطاء بشكل صحيح في دوال جلب البيانات الخاصة بك:
// app/posts/page.tsx
async function getPosts() {
try {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
});
if (!res.ok) {
throw new Error(`خطأ HTTP! الحالة: ${res.status}`);
}
return res.json();
} catch (error) {
console.error('فشل جلب المنشورات:', error);
return []; // إرجاع بيانات احتياطية
}
}
export default async function PostsPage() {
const posts = await getPosts();
if (posts.length === 0) {
return (
<div>
<h1>المنشورات</h1>
<p>لا توجد منشورات متاحة في الوقت الحالي.</p>
</div>
);
}
return (
<div>
<h1>منشورات المدونة</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
أنماط جلب البيانات
النمط 1: الجلب في التخطيط والصفحة
// app/blog/layout.tsx
async function getCategories() {
const res = await fetch('https://api.example.com/categories');
return res.json();
}
export default async function BlogLayout({ children }) {
const categories = await getCategories();
return (
<div>
<aside>
<h3>الفئات</h3>
<ul>
{categories.map(cat => (
<li key={cat.id}>{cat.name}</li>
))}
</ul>
</aside>
<main>{children}</main>
</div>
);
}
// app/blog/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
}
النمط 2: الجلب مع معلمات البحث
// app/search/page.tsx
async function searchProducts(query: string, category?: string) {
const params = new URLSearchParams({
q: query,
...(category && { category })
});
const res = await fetch(`https://api.example.com/search?${params}`, {
cache: 'no-store' // نتائج البحث يجب أن تكون طازجة
});
return res.json();
}
export default async function SearchPage({
searchParams
}: {
searchParams: { q: string; category?: string }
}) {
const { q, category } = searchParams;
if (!q) {
return <div>أدخل استعلام بحث</div>;
}
const results = await searchProducts(q, category);
return (
<div>
<h1>نتائج البحث عن "{q}"</h1>
{results.length === 0 ? (
<p>لم يتم العثور على نتائج</p>
) : (
<ul>
{results.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)}
</div>
);
}
استعلامات قاعدة البيانات
يمكنك الاستعلام عن قواعد البيانات مباشرة في مكونات الخادم باستخدام ORMs مثل Prisma:
// app/posts/page.tsx
import { prisma } from '@/lib/prisma';
export default async function PostsPage() {
const posts = await prisma.post.findMany({
where: {
published: true
},
include: {
author: {
select: {
name: true,
avatar: true
}
}
},
orderBy: {
createdAt: 'desc'
},
take: 20
});
return (
<div>
<h1>أحدث المنشورات</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>بواسطة {post.author.name}</p>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
لا حاجة لمسارات API: مع مكونات الخادم، يمكنك تخطي إنشاء مسارات API لجلب البيانات. استعلم عن قاعدة بياناتك مباشرة في المكونات، مما يقلل من تعقيد الكود ويحسن الأداء.
حفظ الطلبات في الذاكرة
يزيل Next.js تلقائياً تكرار طلبات fetch المتطابقة أثناء ممر عرض واحد:
// مكونات متعددة يمكنها استدعاء نفس الدالة
async function getUser() {
const res = await fetch('https://api.example.com/user');
return res.json();
}
// app/page.tsx
async function Header() {
const user = await getUser(); // الاستدعاء الأول
return <header>{user.name}</header>;
}
async function Sidebar() {
const user = await getUser(); // مزال التكرار
return <aside>{user.email}</aside>;
}
export default function Page() {
return (
<>
<Header />
<Sidebar />
</>
);
}
// النتيجة: يتم إجراء طلب fetch واحد فقط!
تمرين عملي
المهمة: أنشئ مدونة بمتطلبات جلب البيانات التالية:
- أنشئ صفحة قائمة منشورات تعيد التحقق كل 5 دقائق
- أنشئ صفحة تفاصيل منشور تجلب بيانات المنشور والمنشورات ذات الصلة بشكل متوازٍ
- نفذ صفحة بحث تجلب نتائج جديدة في كل بحث
- أضف مسار API لتشغيل إعادة التحقق عند الطلب لمنشورات معينة
- أنشئ لوحة تحكم تجلب بيانات المستخدم والتحليلات والنشاط الأخير بشكل متوازٍ
المتطلبات:
- استخدم استراتيجيات التخزين المؤقت المناسبة لكل صفحة
- نفذ معالجة الأخطاء الصحيحة
- استخدم علامات التخزين المؤقت لإعادة التحقق الدقيقة
- قم بتحسين جلب البيانات المتوازي حيثما ينطبق
- أضف أنواع TypeScript لجميع هياكل البيانات
التحدي الإضافي: نفذ التحميل المسبق لصفحة تفاصيل المنشور لبدء جلب البيانات في التخطيط قبل عرض مكون الصفحة.
الملخص
- Next.js يوسع fetch() مع التخزين المؤقت التلقائي وإزالة التكرار
- استخدم cache: 'force-cache' (افتراضي) للبيانات الثابتة، 'no-store' للبيانات الديناميكية
- نفذ إعادة التحقق المستندة إلى الوقت مع next: { revalidate: seconds }
- استخدم إعادة التحقق عند الطلب مع revalidatePath() و revalidateTag()
- اجلب البيانات بشكل متوازٍ مع Promise.all() لتحسين الأداء
- استخدم الجلب التسلسلي عندما يعتمد طلب واحد على آخر
- نفذ نمط التحميل المسبق لجلب البيانات المبكر
- استعلم عن قواعد البيانات مباشرة في مكونات الخادم بدون مسارات API
- Next.js يزيل تلقائياً تكرار طلبات fetch المتطابقة في عرض واحد
- نفذ دائماً معالجة الأخطاء الصحيحة في دوال جلب البيانات