كيفية بناء تطبيقات الصفحة الواحدة (SPAs) باستخدام Next.js
يدعم Next.js بشكل كامل بناء تطبيقات الصفحة الواحدة (Single-Page Applications - SPAs).
يشمل ذلك انتقالات سريعة بين المسارات مع الجلب المسبق (prefetching)، جلب البيانات من جانب العميل، استخدام واجهات برمجة التطبيقات (APIs) للمتصفح، التكامل مع مكتبات الجهات الخارجية للعميل، إنشاء مسارات ثابتة، والمزيد.
إذا كان لديك تطبيق صفحة واحدة موجود بالفعل، يمكنك الانتقال إلى Next.js دون إجراء تغييرات كبيرة على الكود الخاص بك. ثم يسمح لك Next.js بإضافة ميزات الخادم تدريجياً حسب الحاجة.
ما هو تطبيق الصفحة الواحدة؟
يختلف تعريف تطبيق الصفحة الواحدة (SPA). سنعرِّف "تطبيق الصفحة الواحدة الصارم" على النحو التالي:
- التصيير من جانب العميل (CSR): يتم تقديم التطبيق بواسطة ملف HTML واحد (مثل
index.html
). يتم التعامل مع كل مسار، انتقال بين الصفحات، وجلب للبيانات بواسطة JavaScript في المتصفح. - لا يوجد إعادة تحميل كاملة للصفحة: بدلاً من طلب مستند جديد لكل مسار، يقوم JavaScript من جانب العميل بتعديل DOM للصفحة الحالية ويجلب البيانات حسب الحاجة.
غالباً ما تتطلب تطبيقات الصفحة الواحدة الصارمة كميات كبيرة من JavaScript لتحميلها قبل أن تصبح الصفحة قابلة للتفاعل. علاوة على ذلك، يمكن أن يكون التعامل مع شلالات البيانات على العميل تحديًا. يمكن لبناء تطبيقات الصفحة الواحدة باستخدام Next.js معالجة هذه المشكلات.
لماذا تستخدم Next.js لتطبيقات الصفحة الواحدة؟
يمكن لـ Next.js تقسيم حزم JavaScript تلقائيًا، وإنشاء نقاط دخول متعددة لملفات HTML في مسارات مختلفة. هذا يتجنب تحميل أكواد JavaScript غير الضرورية على جانب العميل، مما يقلل حجم الحزمة ويُمكن من تحميل الصفحات بشكل أسرع.
مكون next/link
يقوم تلقائيًا بجلب المسارات مسبقًا، مما يمنحك انتقالات سريعة بين الصفحات كما في تطبيق الصفحة الواحدة الصارم، ولكن مع ميزة الحفاظ على حالة توجيه التطبيق في عنوان URL للمشاركة والربط.
يمكن أن يبدأ Next.js كموقع ثابت أو حتى كتطبيق صفحة واحدة صارم حيث يتم تصيير كل شيء من جانب العميل. إذا نما مشروعك، يسمح لك Next.js بإضافة ميزات الخادم تدريجياً (مثل مكونات خادم React، أفعال الخادم، والمزيد) حسب الحاجة.
أمثلة
لنستكشف الأنماط الشائعة المستخدمة في بناء تطبيقات الصفحة الواحدة وكيف يحلها Next.js.
استخدام خطاف use
من React داخل موفر السياق (Context Provider)
نوصي بجلب البيانات في مكون رئيسي (أو تخطيط)، وإرجاع الوعد (Promise)، ثم فك القيمة في مكون العميل باستخدام خطاف use
من React.
يمكن لـ Next.js بدء جلب البيانات مبكرًا على الخادم. في هذا المثال، هذا هو التخطيط الجذري - نقطة الدخول إلى تطبيقك. يمكن للخادم البدء فوريًا في بث الاستجابة إلى العميل.
عن طريق "رفع" جلب البيانات إلى التخطيط الجذري، يبدأ Next.js الطلبات المحددة على الخادم مبكرًا قبل أي مكونات أخرى في تطبيقك. هذا يلغي شلالات العميل ويمنع حدوث جولات متعددة بين العميل والخادم. يمكن أن يحسن الأداء بشكل كبير، حيث يكون خادمك أقرب (ويفضل أن يكون موجودًا في نفس المكان) إلى قاعدة البيانات الخاصة بك.
على سبيل المثال، قم بتحديث التخطيط الجذري الخاص بك لاستدعاء الوعد، ولكن لا تنتظره.
import { UserProvider } from './user-provider'
import { getUser } from './user' // بعض الوظائف من جانب الخادم
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
let userPromise = getUser() // لا تنتظر هنا
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
import { UserProvider } from './user-provider'
import { getUser } from './user' // بعض الوظائف من جانب الخادم
export default function RootLayout({ children }) {
let userPromise = getUser() // لا تنتظر هنا
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
بينما يمكنك تأجيل وإرسال وعد واحد كخاصية إلى مكون العميل، نرى عادةً هذا النمط مقترنًا بموفر سياق React. هذا يمكّن الوصول الأسهل من مكونات العميل باستخدام خطاف React مخصص.
يمكنك إعادة توجيه الوعد إلى موفر سياق React:
'use client';
import { createContext, useContext, ReactNode } from 'react';
type User = any;
type UserContextType = {
userPromise: Promise<User | null>;
};
const UserContext = createContext<UserContextType | null>(null);
export function useUser(): UserContextType {
let context = useContext(UserContext);
if (context === null) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
export function UserProvider({
children,
userPromise
}: {
children: ReactNode;
userPromise: Promise<User | null>;
}) {
return (
<UserContext.Provider value={{ userPromise }}>
{children}
</UserContext.Provider>
);
}
'use client'
import { createContext, useContext, ReactNode } from 'react'
const UserContext = createContext(null)
export function useUser() {
let context = useContext(UserContext)
if (context === null) {
throw new Error('useUser must be used within a UserProvider')
}
return context
}
export function UserProvider({ children, userPromise }) {
return (
<UserContext.Provider value={{ userPromise }}>
{children}
</UserContext.Provider>
)
}
أخيرًا، يمكنك استدعاء الخطاف المخصص useUser()
في أي مكون عميل وفك الوعد:
'use client'
import { use } from 'react'
import { useUser } from './user-provider'
export function Profile() {
const { userPromise } = useUser()
const user = use(userPromise)
return '...'
}
'use client'
import { use } from 'react'
import { useUser } from './user-provider'
export function Profile() {
const { userPromise } = useUser()
const user = use(userPromise)
return '...'
}
سيتم تعليق المكون الذي يستهلك الوعد (مثل Profile
أعلاه). هذا يمكّن الترطيب الجزئي. يمكنك رؤية HTML الذي تم بثه وتصييره مسبقًا قبل انتهاء تحميل JavaScript.
تطبيقات الصفحة الواحدة مع SWR
SWR هي مكتبة React شائعة لجلب البيانات.
مع SWR 2.3.0 (و React 19+)، يمكنك تبني ميزات الخادم تدريجياً بجانب كود جلب البيانات الحالي المعتمد على SWR. هذا تجريد للنمط السابق لـ use()
. هذا يعني أنه يمكنك نقل جلب البيانات بين العميل وجانب الخادم، أو استخدام كليهما:
- العميل فقط:
useSWR(key, fetcher)
- الخادم فقط:
useSWR(key)
+ بيانات مقدمة من RSC - مختلط:
useSWR(key, fetcher)
+ بيانات مقدمة من RSC
على سبيل المثال، قم بتغليف تطبيقك بـ <SWRConfig>
و fallback
:
import { SWRConfig } from 'swr'
import { getUser } from './user' // بعض الوظائف من جانب الخادم
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<SWRConfig
value={{
fallback: {
// لا ننتظر getUser() هنا
// فقط المكونات التي تقرأ هذه البيانات سيتم تعليقها
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}
import { SWRConfig } from 'swr'
import { getUser } from './user' // بعض الوظائف من جانب الخادم
export default function RootLayout({ children }) {
return (
<SWRConfig
value={{
fallback: {
// لا ننتظر getUser() هنا
// فقط المكونات التي تقرأ هذه البيانات سيتم تعليقها
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}
نظرًا لأن هذا مكون خادم، يمكن لـ getUser()
قراءة ملفات تعريف الارتباط (cookies) أو الرؤوس (headers) أو التحدث إلى قاعدة البيانات الخاصة بك بشكل آمن. لا حاجة إلى مسار API منفصل. يمكن لمكونات العميل أسفل <SWRConfig>
استدعاء useSWR()
بنفس المفتاح لاسترداد بيانات المستخدم. لا يتطلب كود المكون مع useSWR
أي تغييرات من حل جلب البيانات الحالي الخاص بالعميل.
'use client'
import useSWR from 'swr'
export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// نفس نمط SWR الذي تعرفه بالفعل
const { data, error } = useSWR('/api/user', fetcher)
return '...'
}
'use client'
import useSWR from 'swr'
export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// نفس نمط SWR الذي تعرفه بالفعل
const { data, error } = useSWR('/api/user', fetcher)
return '...'
}
يمكن تصيير بيانات fallback
مسبقًا وتضمينها في استجابة HTML الأولية، ثم قراءتها فورًا في المكونات الفرعية باستخدام useSWR
. لا تزال استطلاعات SWR وإعادة التحقق والتخزين المؤقت تعمل من جانب العميل فقط، لذا فهي تحافظ على كل التفاعلية التي تعتمد عليها لتطبيق الصفحة الواحدة.
نظرًا لأن بيانات fallback
الأولية يتم التعامل معها تلقائيًا بواسطة Next.js، يمكنك الآن حذف أي منطق شرطي كان مطلوبًا سابقًا للتحقق مما إذا كانت data
غير محددة (undefined
). عندما تكون البيانات قيد التحميل، سيتم تعليق حدود <Suspense>
الأقرب.
SWR | RSC | RSC + SWR | |
---|---|---|---|
بيانات SSR | |||
البث أثناء SSR | |||
إزالة تكرار الطلبات | |||
ميزات جانب العميل |
تطبيقات الصفحة الواحدة مع React Query
يمكنك استخدام React Query مع Next.js على كل من العميل والخادم. هذا يمكّنك من بناء تطبيقات الصفحة الواحدة الصارمة، وكذلك الاستفادة من ميزات الخادم في Next.js مع React Query.
تعلم المزيد في توثيق React Query.
تصيير المكونات فقط في المتصفح
يتم تصيير مكونات العميل مسبقًا أثناء next build
. إذا كنت تريد تعطيل التصيير المسبق لمكون العميل وتحميله فقط في بيئة المتصفح، يمكنك استخدام next/dynamic
:
import dynamic from 'next/dynamic'
const ClientOnlyComponent = dynamic(() => import('./component'), {
ssr: false,
})
يمكن أن يكون هذا مفيدًا لمكتبات الجهات الخارجية التي تعتمد على واجهات برمجة التطبيقات (APIs) للمتصفح مثل window
أو document
. يمكنك أيضًا إضافة useEffect
يتحقق من وجود هذه الواجهات، وإذا لم تكن موجودة، يُرجع null
أو حالة تحميل سيتم تصييرها مسبقًا.
التوجيه السطحي (Shallow Routing) على العميل
إذا كنت تنتقل من تطبيق صفحة واحدة صارم مثل Create React App أو Vite، قد يكون لديك كود موجود يقوم بالتوجيه السطحي لتحديث حالة عنوان URL. يمكن أن يكون هذا مفيدًا للانتقالات اليدوية بين المشاهدات في تطبيقك بدون استخدام توجيه نظام الملفات الافتراضي لـ Next.js.
يسمح لك Next.js باستخدام الطرق الأصلية window.history.pushState
و window.history.replaceState
لتحديث سجل المتصفح دون إعادة تحميل الصفحة.
تكاملات pushState
و replaceState
مع موجه Next.js، مما يسمح لك بالمزامنة مع usePathname
و useSearchParams
.
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const urlSearchParams = new URLSearchParams(searchParams.toString())
urlSearchParams.set('sort', sortOrder)
window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>ترتيب تصاعدي</button>
<button onClick={() => updateSorting('desc')}>ترتيب تنازلي</button>
</>
)
}
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder) {
const urlSearchParams = new URLSearchParams(searchParams.toString())
urlSearchParams.set('sort', sortOrder)
window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>ترتيب تصاعدي</button>
<button onClick={() => updateSorting('desc')}>ترتيب تنازلي</button>
</>
)
}
تعلم المزيد حول كيفية عمل التوجيه والتنقل في Next.js.
استخدام أفعال الخادم (Server Actions) في مكونات العميل
يمكنك تبني أفعال الخادم تدريجياً مع الاستمرار في استخدام مكونات العميل. هذا يسمح لك بإزالة الكود المتكرر لاستدعاء مسار API، واستبداله بميزات React مثل useActionState
للتعامل مع حالات التحميل والخطأ.
على سبيل المثال، أنشئ أول فعل خادم:
'use server'
export async function create() {}
'use server'
export async function create() {}
يمكنك استيراد واستخدام فعل الخادم من العميل، بشكل مشابه لاستدعاء دالة JavaScript. لا تحتاج إلى إنشاء نقطة نهاية API يدويًا:
تعلم المزيد حول تعديل البيانات بأفعال الخادم.
التصدير الثابت (اختياري)
يدعم Next.js أيضًا إنشاء موقع ثابت بالكامل. هذا له بعض المزايا مقارنة بتطبيقات الصفحة الواحدة الصارمة:
- تقسيم الكود التلقائي: بدلاً من إرسال ملف
index.html
واحد، سينشئ Next.js ملف HTML لكل مسار، بحيث يحصل الزوار على المحتوى بشكل أسرع دون انتظار حزمة JavaScript للعميل. - تحسين تجربة المستخدم: بدلاً من هيكل أساسي لجميع المسارات، تحصل على صفحات مكتملة التصيير لكل مسار. عندما يتنقل المستخدمون من جانب العميل، تظل الانتقالات فورية وشبيهة بتطبيقات الصفحة الواحدة.
لتمكين التصدير الثابت، قم بتحديث التكوين الخاص بك:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'export',
}
export default nextConfig
بعد تشغيل next build
، سينشئ Next.js مجلد out
مع أصول HTML/CSS/JS لتطبيقك.
ملاحظة: لا تدعم ميزات خادم Next.js التصدير الثابت. تعلم المزيد.
نقل المشاريع الحالية إلى Next.js
يمكنك الانتقال تدريجياً إلى Next.js باتباع أدلتنا:
إذا كنت تستخدم بالفعل تطبيق صفحة واحدة مع موجه الصفحات، يمكنك تعلم كيفية تبني موجه التطبيق تدريجياً.