Backالعودة إلى المدونة

التخزين المؤقت القابل للتكوين مع Next.js

تعرف على تصميم واجهة برمجة التطبيقات (API) ومزايا التوجيه 'use cache'

نحن نعمل على نموذج بسيط وقوي للتخزين المؤقت لـ Next.js. في منشور سابق، تحدثنا عن رحلتنا مع التخزين المؤقت وكيف توصلنا إلى توجيه 'use cache'.

سيناقش هذا المنشور تصميم واجهة برمجة التطبيقات (API) ومزايا 'use cache'.

ما هو 'use cache'؟

'use cache' يجعل تطبيقك أسرع عن طريق تخزين البيانات أو المكونات مؤقتًا عند الحاجة.

إنه "توجيه" (directive) في JavaScript - عبارة نصية تضيفها في الكود الخاص بك - والتي تشير إلى مترجم Next.js للدخول إلى "حدود" مختلفة. على سبيل المثال، الانتقال من الخادم إلى العميل.

هذه فكرة مشابهة لتوجيهات React مثل 'use client' و 'use server'. التوجيهات هي تعليمات المترجم التي تحدد مكان تشغيل الكود، مما يسمح للإطار بتحسين وتنسيق الأجزاء الفردية لك.

كيف يعمل؟

لنبدأ بمثال بسيط:

async function getUser(id) {
  'use cache';
  let res = await fetch(`https://api.vercel.app/user/${id}`);
  return res.json();
}

خلف الكواليس، يحول Next.js هذا الكود إلى دالة خادم بسبب توجيه 'use cache'. أثناء الترجمة، يتم العثور على "التبعيات" (dependencies) لإدخال التخزين المؤقت هذه واستخدامها كجزء من مفتاح التخزين المؤقت.

على سبيل المثال، يصبح id جزءًا من مفتاح التخزين المؤقت. إذا استدعينا getUser(1) عدة مرات، فإننا نعيد الإخراج المخزن مؤقتًا من دالة الخادم. تغيير هذه القيمة سينشئ إدخالًا جديدًا في التخزين المؤقت.

لنلق نظرة على مثال باستخدام الدالة المخزنة مؤقتًا في مكون خادم مع إغلاق.

function Profile({ id }) {
  async function getNotifications(index, limit) {
    'use cache';
    return await db
      .select()
      .from(notifications)
      .limit(limit)
      .offset(index)
      .where(eq(notifications.userId, id));
  }
 
  return <User notifications={getNotifications} />;
}

هذا المثال أكثر صعوبة. هل يمكنك تحديد جميع التبعيات التي يجب أن تكون جزءًا من مفتاح التخزين المؤقت؟

الوسائط index و limit منطقية - إذا تغيرت هذه القيم، نختار شريحة مختلفة من الإشعارات. ولكن ماذا عن معرف المستخدم id؟ قيمته تأتي من المكون الأصلي.

يمكن للمترجم فهم أن getNotifications يعتمد أيضًا على id، ويتم تضمين قيمته تلقائيًا في مفتاح التخزين المؤقت. هذا يمنع فئة كاملة من مشاكل التخزين المؤقت الناتجة عن تبعيات غير صحيحة أو مفقودة في مفتاح التخزين المؤقت.

لماذا لا نستخدم دالة تخزين مؤقت؟

لنراجع المثال الأخير. هل يمكننا بدلاً من ذلك استخدام دالة cache() بدلاً من التوجيه؟

function Profile({ id }) {
  async function getNotifications(index, limit) {
    return await cache(async () => {
      return await db
        .select()
        .from(notifications)
        .limit(limit)
        .offset(index)
        // خطأ! أين ندرج id في مفتاح التخزين المؤقت؟
        .where(eq(notifications.userId, id));
    });
  }
 
  return <User notifications={getNotifications} />;
}

لن تتمكن دالة cache() من النظر داخل الإغلاق ورؤية أن قيمة id يجب أن تكون جزءًا من مفتاح التخزين المؤقت. ستحتاج إلى تحديد يدويًا أن id جزء من مفتاحك. إذا نسيت القيام بذلك، أو فعلت ذلك بشكل غير صحيح، فإنك تخاطر بتصادم التخزين المؤقت أو بيانات قديمة.

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

يمنح 'use cache' المترجم سياقًا كافيًا للتعامل مع الإغلاقات بأمان وإنشاء مفاتيح التخزين المؤقت بشكل صحيح. الحل الذي يعمل في وقت التشغيل فقط، مثل cache()، سيتطلب منك القيام بكل شيء يدويًا - ومن السهل ارتكاب الأخطاء. في المقابل، يمكن تحليل التوجيه بشكل ثابت للتعامل مع جميع تبعياتك بشكل موثوق تحت الغطاء.

كيف يتم التعامل مع قيم الإدخال غير القابلة للتسلسل؟

لدينا نوعان مختلفان من قيم الإدخال للتخزين المؤقت:

  • قابلة للتسلسل (Serializable): هنا، "قابلة للتسلسل" تعني أنه يمكن تحويل الإدخال إلى تنسيق مستند إلى سلسلة ومستقر بدون فقدان المعنى. بينما يفكر الكثيرون أولاً في JSON.stringify، فإننا نستخدم في الواقع تسلسل React (على سبيل المثال، عبر مكونات الخادم) للتعامل مع نطاق أوسع من المدخلات - بما في ذلك الوعود، وهياكل البيانات الدائرية، والكائنات المعقدة الأخرى. هذا يتجاوز ما يمكن لـ JSON العادي القيام به.
  • غير قابلة للتسلسل (Non-serializable): هذه المدخلات ليست جزءًا من مفتاح التخزين المؤقت. عندما نحاول تخزين هذه القيم مؤقتًا، نعيد "مرجع" الخادم. ثم يستخدم Next.js هذا المرجع لاستعادة القيمة الأصلية في وقت التشغيل.

لنفترض أننا تذكرنا تضمين id في مفتاح التخزين المؤقت:

await cache(async () => {
  return await db
    .select()
    .from(notifications)
    .limit(limit)
    .offset(index)
    .where(eq(notifications.userId, id));
}, [id, index, limit]);

هذا يعمل إذا كانت قيم الإدخال قابلة للتسلسل. ولكن إذا كان id عنصر React أو قيمة أكثر تعقيدًا، فسيتعين علينا تسلسل مفاتيح الإدخال يدويًا. ضع في اعتبارك مكون خادم يحصل على المستخدم الحالي بناءً على خاصية id:

async function Profile({ id, children }) {
  'use cache';
  const user = await getUser(id);
 
  return (
    <>
      <h1>{user.name}</h1>
      {/* تغيير children لا يكسر التخزين المؤقت... لماذا؟ */}
      {children}
    </>
  );
}

لنتابع كيف يعمل هذا:

  1. أثناء الترجمة، يرى Next.js توجيه 'use cache' ويحول الكود لإنشاء دالة خادم خاصة تدعم التخزين المؤقت. لا يحدث التخزين المؤقت أثناء الترجمة، ولكن Next.js يقوم بإعداد الآلية اللازمة للتخزين المؤقت في وقت التشغيل.
  2. عندما يستدعي الكود الخاص بك "دالة التخزين المؤقت"، يقوم Next.js بتسلسل وسائط الدالة. أي شيء غير قابل للتسلسل مباشرة، مثل JSX، يتم استبداله بـ "مرجع" كعنصر نائب.
  3. يتحقق Next.js مما إذا كانت هناك نتيجة مخزنة مؤقتًا لوسائط التسلسل المحددة. إذا لم يتم العثور على نتيجة، تحسب الدالة القيمة الجديدة للتخزين المؤقت.
  4. بعد انتهاء الدالة، يتم تسلسل قيمة الإرجاع. يتم تحويل الأجزاء غير القابلة للتسلسل من قيمة الإرجاع إلى مراجع مرة أخرى.
  5. يقوم الكود الذي استدعى دالة التخزين المؤقت بإلغاء تسلسل الإخراج وتقييم المراجع. هذا يسمح لـ Next.js بتبديل المراجع بقيمها أو كائناتها الأصلية، مما يعني أن المدخلات غير القابلة للتسلسل مثل children يمكنها الاحتفاظ بقيمها الأصلية غير المخزنة مؤقتًا.

هذا يعني أنه يمكننا تخزين مكون <Profile> فقط مؤقتًا وليس الأطفال. في عمليات التقديم اللاحقة، لا يتم استدعاء getUser() مرة أخرى. قد تكون قيمة children ديناميكية أو عنصرًا مخزنًا مؤقتًا بشكل منفصل بعمر تخزين مؤقت مختلف. هذا هو التخزين المؤقت القابل للتكوين.

يبدو هذا مألوفًا...

إذا كنت تفكر "هذا يشبه نفس نموذج تكوين الخادم والعميل" - فأنت على حق تمامًا. يُطلق على هذا أحيانًا اسم نمط "الدونات":

  • الجزء الخارجي من الدونات هو مكون خادم يتعامل مع جلب البيانات أو المنطق الثقيل.
  • الثقب في المنتصف هو مكون فرعي قد يكون به بعض التفاعل
app/page.tsx
export default function Page() {
  return (
    <ServerComponent>
      {/* إنشاء ثقب إلى العميل */}
      <ClientComponent />
    <ServerComponent />
  );
}

'use cache' هو نفسه. الدونات هي القيمة المخزنة مؤقتًا للمكون الخارجي والثقب هو المراجع التي يتم ملؤها في وقت التشغيل. هذا هو السبب في أن تغيير children لا يبطل إخراج التخزين المؤقت بالكامل. الأطفال هم مجرد بعض المراجع التي يتم ملؤها لاحقًا.

ماذا عن الوسم والإبطال؟

يمكنك تحديد عمر التخزين المؤقت باستخدام ملفات تعريف مختلفة. نضمن مجموعة من الملفات الافتراضية، ولكن يمكنك تحديد قيم مخصصة إذا رغبت في ذلك.

async function getUser(id) {
  'use cache';
  cacheLife('hours');
  let res = await fetch(`https://api.vercel.app/user/${id}`);
  return res.json();
}

لبطل إدخال تخزين مؤقت معين، يمكنك وسم التخزين المؤقت ثم استدعاء revalidateTag(). نمط قوي واحد هو أنه يمكنك وسم التخزين المؤقت بعد جلب بياناتك (على سبيل المثال من نظام إدارة المحتوى):

async function getPost(postId) {
  'use cache';
  let res = await fetch(`https://api.vercel.app/blog/${postId}`);
  let data = await res.json();
  cacheTag(postId, data.authorId);
  return data;
}

بسيط وقوي

هدفنا مع 'use cache' هو جعل كتابة منطق التخزين المؤقت بسيطًا وقويًا.

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

'use cache لا يزال تجريبيًا داخل Next.js. نود الحصول على ملاحظاتك المبكرة أثناء اختباره.

تعلم المزيد في الوثائق.