كيفية تنفيذ التخزين المؤقت في Node.js باستخدام Redis

اختار المؤلف /dev/color لتلقي تبرع كجزء من برنامج الكتابة من أجل التبرعات.

مقدمة

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

للتغلب على هذه المشاكل، يمكنك تخزين بياناتك في ذاكرة التخزين المؤقت بحيث يقوم التطبيق بعملية طلب واحدة إلى واجهة برمجة التطبيقات، وستسترد الطلبات البيانات فيما بعد من الذاكرة المؤقتة. Redis، قاعدة بيانات في الذاكرة تخزن البيانات في ذاكرة الخادم، هي أداة شائعة لتخزين البيانات. يمكنك الاتصال بـ Redis في Node.js باستخدام الوحدة النمطية node-redis، والتي تمنحك طرقًا لاسترداد وتخزين البيانات في Redis.

في هذا البرنامج التعليمي، ستقوم ببناء تطبيق Express الذي يسترد البيانات من واجهة برمجة التطبيقات RESTful باستخدام وحدة الـaxios. بعد ذلك، ستقوم بتعديل التطبيق لتخزين البيانات المُحمَلة من الواجهة البرمجية في Redis باستخدام وحدة node-redis. بعد ذلك، ستقوم بتنفيذ فترة صلاحية الذاكرة المؤقتة بحيث يمكن للذاكرة المؤقتة أن تنتهي بعد مرور فترة زمنية معينة. وأخيرًا، ستستخدم وسيط Express لتخزين البيانات في الذاكرة المؤقتة.

المتطلبات المسبقة

لمتابعة البرنامج التعليمي، ستحتاج إلى:

الخطوة 1 — إعداد المشروع

في هذه الخطوة، ستقوم بتثبيت التبعيات الضرورية لهذا المشروع وبدء خادم Express. في هذا البرنامج التعليمي، ستقوم بإنشاء ويكي يحتوي على معلومات حول أنواع مختلفة من الأسماك. سنطلق على المشروع اسم fish_wiki.

أولاً، قم بإنشاء الدليل للمشروع باستخدام أمر mkdir:

  1. mkdir fish_wiki

انتقل إلى الدليل:

  1. cd fish_wiki

تهيئة ملف package.json باستخدام أمر npm:

  1. npm init -y

الخيار -y يقبل جميع القيم الافتراضية تلقائيًا.

عند تشغيل أمر npm init، سيقوم بإنشاء ملف package.json في الدليل الخاص بك بالمحتويات التالية:

Output
Wrote to /home/your_username/<^>fish_wiki<^/package.json: { "name": "fish_wiki", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

بعد ذلك، ستقوم بتثبيت الحزم التالية:

  • express: إطار عمل لخادم الويب لـ Node.js.
  • axios: عميل HTTP لـ Node.js، والذي يفيد في إجراءات استدعاء الواجهة البرمجية للتطبيقات.
  • node-redis: عميل Redis الذي يتيح لك تخزين البيانات والوصول إليها في Redis.

لتثبيت الحزم الثلاث معًا، أدخل الأمر التالي:

  1. npm install express axios redis

بعد تثبيت الحزم، ستقوم بإنشاء خادم Express أساسي.

باستخدام nano أو محرر النص الذي تفضله، أنشئ وافتح ملف server.js:

  1. nano server.js

في ملف server.js الخاص بك، أدخل الكود التالي لإنشاء خادم Express:

fish_wiki/server.js
const express = require("express");

const app = express();
const port = process.env.PORT || 3000;


app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

أولاً، استورد express في الملف. في السطر الثاني، قم بتعيين المتغير app كنسخة من express، مما يمنحك الوصول إلى الوسائل مثل get، post، listen، والمزيد. سيتمركز هذا البرنامج التعليمي على الوسائل get و listen.

في السطر التالي، قم بتعريف المتغير port وتعيينه لرقم المنفذ الذي تريد أن يستمع إليه الخادم. إذا لم يتوفر رقم منفذ في ملف المتغيرات البيئية، فسيتم استخدام المنفذ 3000 كافتراضي.

أخيرًا ، باستخدام المتغير app ، قم باستدعاء طريقة listen() لوحدة البرنامج express لبدء الخادم على المنفذ 3000.

احفظ وأغلق الملف.

شغل ملف server.js باستخدام أمر node لبدء الخادم:

  1. node server.js

سيتم تسجيل رسالة مماثلة للتالية في وحدة التحكم:

Output
App listening on port 3000

الإخراج يؤكد أن الخادم يعمل وجاهز لخدمة أي طلبات على المنفذ 3000. نظرًا لأن Node.js لا يعيد تحميل الخادم تلقائيًا عند تغيير الملفات ، ستقوم الآن بإيقاف الخادم باستخدام CTRL+C حتى تتمكن من تحديث server.js في الخطوة التالية.

بمجرد تثبيت الاعتماديات وإنشاء خادم Express ، ستسترد البيانات من واجهة برمجة التطبيقات (API) RESTful.

الخطوة 2 — استرداد البيانات من واجهة برمجة التطبيقات (API) RESTful دون تخزين مؤقت

في هذه الخطوة ، ستبني على الخادم Express من الخطوة السابقة لاسترداد البيانات من واجهة برمجة التطبيقات (API) RESTful دون تنفيذ تخزين مؤقت ، مما يوضح ما يحدث عندما لا تتم تخزين البيانات في ذاكرة التخزين المؤقت.

للبدء ، افتح ملف server.js في محرر النص الخاص بك:

  1. nano server.js

بعد ذلك ، ستقوم باسترداد البيانات من واجهة برمجة التطبيقات (API) FishWatch. توفر واجهة برمجة التطبيقات (API) FishWatch معلومات حول أنواع الأسماك.

في ملف server.js، قم بتعريف دالة تُطلب بيانات واجهة برمجة التطبيقات باستخدام الكود المميز التالي:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");

const app = express();
const port = process.env.PORT || 3000;

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

في السطر الثاني، قم بإستيراد وحدة axios. بعد ذلك، قم بتعريف دالة غير متزامنة fetchApiData()، التي تأخذ species كمعلمة. لجعل الدالة غير متزامنة، ضع الكلمة المفتاحية async قبلها.

ضمن الدالة، قم باستدعاء طريقة get() من وحدة axios باستخدام عنوان واجهة برمجة التطبيقات التي تريد أن تقوم الطريقة بجلب البيانات منها، وهو واجهة برمجة التطبيقات FishWatch في هذا المثال. بما أن طريقة get() تنفذ وعدًا promise، ضع الكلمة المفتاحية await لحل الوعد. بمجرد حل الوعد وإرجاع البيانات من واجهة برمجة التطبيقات، قم باستدعاء طريقة console.log(). ستسجل طريقة console.log() رسالة تقول إنه تم إرسال طلب إلى واجهة برمجة التطبيقات. أخيرًا، قم بإرجاع البيانات من واجهة برمجة التطبيقات.

بعد ذلك، ستقوم بتعريف مسار Express يقبل طلبات GET. في ملف server.js، قم بتعريف المسار بالكود التالي:

fish_wiki/server.js
...

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  ...
});

في الكود السابق، قم باستدعاء طريقة get() من وحدة express، التي تستمع فقط على طلبات GET. تأخذ الطريقة معها معلمتين:

  • /fish/:species: النقطة النهائية التي ستكون Express تستمع إليها. النقطة النهائية تأخذ معلمة مسار :species التي تلتقط أي شيء يتم إدخاله في تلك الموضع في عنوان URL.
  • getSpeciesData()(لم يتم تعريفها بعد): وظيفة استدعاء ستتم استدعاؤها عندما يتطابق عنوان URL مع النقطة النهائية المحددة في الوسيطة الأولى.

الآن بعد تعريف المسار، حدد وظيفة استدعاء getSpeciesData:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
}
app.get("/fish/:species", getSpeciesData);
...

الدالة getSpeciesData هي دالة معالجة غير متزامنة تمرر إلى الطريقة get() في وحدة express كوسيط ثانوي. تأخذ دالة getSpeciesData() معها معلمتين: كائن الطلب (request) وكائن الاستجابة (response). كائن الطلب يحتوي على معلومات حول العميل، بينما يحتوي كائن الاستجابة على المعلومات التي سيتم إرسالها إلى العميل من Express.

بعد ذلك، أضف الكود المظلل لاستدعاء fetchApiData() لاسترداد البيانات من واجهة برمجة التطبيقات في وظيفة استدعاء getSpeciesData():

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  results = await fetchApiData(species);
}
...

في الدالة، تستخرج القيمة التي تم التقاطها من النقطة النهائية المخزنة في كائن req.params، ثم تسند إلى المتغير species. في السطر التالي، تعرف المتغير results وتعيده إلى undefined.

بعد ذلك، تقوم بإستدعاء الدالة fetchApiData() مع المتغير species كوسيط. إستدعاء دالة fetchApiData() مسبوق ببنية await لأنها تعيد وعدًا. عندما يتم حل الوعد، يُعيد البيانات التي تُسند بعدها إلى المتغير results.

بعد ذلك، قم بإضافة الكود المظلل للتعامل مع أخطاء التشغيل:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

تقوم بتعريف الكتلة try/catch للتعامل مع أخطاء التشغيل. في الكتلة try، تقوم بإستدعاء fetchApiData() لاسترداد البيانات من واجهة برمجة التطبيقات.
إذا تم إكتشاف خطأ، تقوم الكتلة catch بتسجيل الخطأ وإرجاع رمز الحالة 404 مع استجابة “البيانات غير متوفرة”.

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

لتشغيل كتلة catch()، تحتاج إلى التحقق مما إذا كانت المصفوفة فارغة ورمي خطأ عندما تُقيم الشرط if بصحة. عندما تُقيم شروط if بخطأ، يمكنك إرسال استجابة إلى العميل تحتوي على البيانات.

للقيام بذلك، أضف الكود المظلل:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  ...
  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

بمجرد عودة البيانات من واجهة برمجة التطبيقات، يقوم البيانات if بفحص متغير results ما إذا كان فارغًا. إذا تم تحقيق الشرط، يتم استخدام البيانات throw لرمي خطأ مخصص برسالة API returned an empty array. بعد تشغيله، يتم تبديل التنفيذ إلى كتلة الامساك catch، التي تسجل رسالة الخطأ وتعيد استجابة 404.

بالمقابل، إذا كان متغير results يحتوي على بيانات، فلن يتم تحقيق شرط البيانات if. نتيجة لذلك، سيتخطى البرنامج كتلة البيانات if وينفذ طريقة send لكائن الاستجابة، الذي يرسل استجابة إلى العميل.

تأخذ طريقة send كائنًا يحتوي على الخصائص التالية:

  • fromCache: تقبل الخاصية قيمة تساعدك في معرفة ما إذا كانت البيانات قادمة من ذاكرة التخزين المؤقت Redis أم من واجهة برمجة التطبيقات. لقد قمت الآن بتعيين قيمة false لأن البيانات تأتي من واجهة برمجة التطبيقات.

  • data: يتم تعيين الخاصية المتغير results الذي يحتوي على البيانات المعادة من واجهة برمجة التطبيقات.

في هذه النقطة، سيكون الكود الخاص بك كاملًا على النحو التالي:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");

const app = express();
const port = process.env.PORT || 3000;

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

الآن بعد أن تم وضع كل شيء في مكانه، قم بحفظ الملف والخروج.

ابدأ خادم express:

  1. node server.js

تقبل واجهة برمجة التطبيقات لرصد الأسماك العديد من الأنواع، ولكننا سنستخدم فقط نوع أسماك red-snapper كمعلمة طريق على النقطة النهائية التي ستقوم باختبارها طوال هذا البرنامج التعليمي.

الآن، قم بتشغيل متصفح الويب المفضل لديك على جهاز الكمبيوتر المحلي الخاص بك. انتقل إلى عنوان URL التالي: http://localhost:3000/fish/red-snapper.

ملاحظة: إذا كنت تتابع البرنامج التعليمي على خادم عن بعد، يمكنك عرض التطبيق في متصفحك باستخدام توجيه المنفذ.

مع تشغيل خادم Node.js لا يزال، افتح محطة أخرى في جهاز الكمبيوتر المحلي الخاص بك، ثم أدخل الأمر التالي:

  1. ssh -L 3000:localhost:3000 your-non-root-user@yourserver-ip

عند الاتصال بالخادم، انتقل إلى عنوان http://localhost:3000/fish/red-snapper على متصفح الويب الخاص بجهازك المحلي.

بمجرد تحميل الصفحة، يجب أن ترى fromCache مضبوطة على false.

الآن، قم بتحديث العنوان URL ثلاث مرات إضافية وانظر إلى المحطة الطرفية الخاصة بك. ستقوم المحطة بتسجيل “تم إرسال طلب إلى خادم الواجهة البرمجية” بقدر مرات تحديث متصفحك.

إذا قمت بتحديث العنوان URL ثلاث مرات بعد الزيارة الأولى، ستبدو النتيجة النهائية كما يلي:

Output
App listening on port 3000 Request sent to the API Request sent to the API Request sent to the API Request sent to the API

تُظهر هذه النتيجة أن طلب الشبكة يتم إرساله إلى خادم واجهة برمجة التطبيقات في كل مرة تقوم فيها بتحديث المتصفح. إذا كان لديك تطبيق يستخدمه 1000 مستخدم يصلون إلى نفس النقطة النهائية، فهذا يعني أنه سيتم إرسال 1000 طلب شبكة إلى واجهة برمجة التطبيقات.

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

للآن، قم بإيقاف خادم Express الخاص بك باستخدام CTRL+C.

الآن بما أنه يمكنك طلب البيانات من واجهة برمجة تطبيقات (API) وتقديمها للمستخدمين، ستخزن البيانات التي تم استرجاعها من واجهة برمجة التطبيقات (API) في Redis.

الخطوة 3 — تخزين طلبات واجهة برمجة التطبيقات RESTful باستخدام Redis

في هذا القسم، ستقوم بتخزين البيانات من واجهة برمجة التطبيقات (API) بحيث يتم طلب البيانات من خادم API فقط في زيارة البداية إلى نقطة نهاية تطبيقك، وستقوم جميع الطلبات التالية بجلب البيانات من ذاكرة التخزين المؤقت Redis.

افتح ملف server.js:

  1. nano server.js

في ملف server.js الخاص بك، استورد وحدة node-redis:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");
...

في نفس الملف، قم بالاتصال بـ Redis باستخدام وحدة node-redis عن طريق إضافة الكود المميز كما في الخطوات التالية:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  ...
}
...

أولاً، قم بتعريف متغير redisClient وتعيين قيمته إلى undefined. بعد ذلك، قم بتعريف دالة غير مسماة تنفذ ذاتياً بطريقة غير متزامنة، وهي دالة تقوم بالتشغيل مباشرة بعد تعريفها. يمكنك تعريف دالة غير مسماة تنفذ ذاتياً عن طريق تقويم تعريف الدالة بدون اسم داخل قوسين (async () => {...}). لجعلها تنفذ ذاتياً، اتبعها مباشرة بمجموعة أخرى من القوسين ()، مما يظهر كـ (async () => {...})().

Dاطلب من داخل الدالة استدعاء طريقة createClient() في وحدة redis التي تنشئ كائن redis. نظرًا لعدم توفيرك للمنفذ الذي يجب على Redis استخدامه عند استدعاء طريقة createClient()، ستستخدم Redis المنفذ 6379، وهو المنفذ الافتراضي.

تستدعي أيضًا طريقة on() في Node.js التي تسجل الأحداث على كائن Redis. تأخذ طريقة on() معها معها معها وسيطتين: error واستدعاءً مرفوع. الوسيط الأول error هو الحدث الذي يتم تنشيطه عندما يواجه Redis خطأ. الوسيط الثاني هو استدعاء يعمل عندما يتم بث الحدث error. يقوم الاستدعاء بتسجيل الخطأ في وحدة التحكم.

أخيرًا، تستدعي طريقة connect()، التي تبدأ الاتصال بـ Redis على المنفذ الافتراضي 6379. تُرجع طريقة connect() وعدًا، لذلك تضيف البادئة await قبلها لحل الوعد.

الآن بمجرد أن تكون التطبيق متصلاً بـ Redis، ستقوم بتعديل استدعاء getSpeciesData() لتخزين البيانات في Redis في الزيارة الأولية واسترداد البيانات من الذاكرة المؤقتة لجميع الطلبات التالية.

في ملف server.js الخاص بك، أضف وقم بتحديث الشيفرة المشار إليها بالإضاءة:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
     }
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    ...
  }
}
...

في الدالة getSpeciesData، تعرف المتغير isCached بقيمة false. ضمن كتلة try، تقوم باستدعاء طريقة get() لوحدة node-redis باستخدام species كوسيط. عندما تجد الطريقة المفتاح في Redis الذي يطابق قيمة المتغير species، يتم إرجاع البيانات، التي يتم تعيينها بعد ذلك إلى المتغير cacheResults.

بعد ذلك، يتحقق البيان if مما إذا كان للمتغير cacheResults بيانات. إذا تم استيفاء الشرط، يتم تعيين المتغير isCache إلى true. بعد ذلك، تستدعي طريقة parse() لكائن JSON بوسيط cacheResults. تقوم طريقة parse() بتحويل بيانات السلسلة JSON إلى كائن JavaScript. بعد جرى تحليل الJSON، تستدعي طريقة send() التي تأخذ كائنًا يحتوي على خاصية fromCache معتمدة على المتغير isCached. تقوم الطريقة بإرسال الاستجابة إلى العميل.

إذا لم تجد طريقة get() في وحدة node-redis أي بيانات في الذاكرة المؤقتة، يتم تعيين المتغير cacheResults إلى null. نتيجة لذلك، يُقَدَّر بأن البيان if كاذبًا. عند حدوث ذلك، يتخطى التنفيذ إلى كتلة else حيث تستدعي دالة fetchApiData() لاسترجاع البيانات من الواجهة البرمجية. ومع ذلك، بمجرد عودة البيانات من الواجهة البرمجية، لا يتم حفظها في Redis.

لحفظ البيانات في ذاكرة التخزين المؤقت Redis، يجب عليك استخدام طريقة set() لوحدة node-redis لحفظها. للقيام بذلك، قم بإضافة السطر المميز كالتالي:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results));
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    ...
  }
}
...

Dentro منطقة ال else , عندما يتم استرجاع البيانات, تقوم بإستدعاء الطريقة set() من وحدة node-redis لحفظ البيانات في Redis تحت اسم المفتاح من قيمة المتغير species.

الطريقة set() تأخذ معها مدخلتين, اللذان هما: زوج مفتاح قيمة species و JSON.stringify(results).

المدخل الأول, species, هو المفتاح الذي سيتم حفظ البيانات تحته في Redis. تذكر species هو معين إلى القيمة المرسلة إلى النقطة النهائية التي قمت بتعريفها. على سبيل المثال, عندما تزور /fish/red-snapper, species يتم تعيينه إلى red-snapper, الذي سيكون المفتاح في Redis.

المدخل الثاني, JSON.stringify(results), هو القيمة للمفتاح. في المدخل الثاني, تستدعي طريقة stringify() من JSON بالمتغير results كمدخل, الذي يحتوي على البيانات التي تم إرجاعها من الAPI. الطريقة تحول JSON إلى سلسلة نصية; هذا هو السبب في أنه, عندما استرجعت البيانات من الذاكرة المخبأة باستخدام طريقة get() من وحدة node-redis سابقًا, قمت باستدعاء طريقة JSON.parse بالمتغير cacheResults كمدخل.

ملفك الكامل الآن سيبدو كما يلي:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results));
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

حفظ واخرج ملفك, وقم بتشغيل server.js بإستخدام الأمر node:

  1. node server.js

بمجرد بدء تشغيل الخادم, قم بتحديث http://localhost:3000/fish/red-snapper في متصفحك.

لاحظ أن fromCache ما زالت معينة إلى false:

الآن قم بتحديث الصفحة مرة أخرى لرؤية أن fromCache مضبوطة إلى true:

قم بتحديث الصفحة خمس مرات وعد إلى الطرفية. سيكون مخرجاتك مشابهة لما يلي:

Output
App listening on port 3000 Request sent to the API

الآن، تم تسجيل Request sent to the API مرة واحدة فقط بعد عدة عمليات تحديث للرابط، متناقضة مع القسم الأخير حيث تم تسجيل الرسالة لكل عملية تحديث. تؤكد هذه المخرجات أن طلب واحد فقط تم إرساله إلى الخادم وبالتالي، يتم جلب البيانات من Redis.

لتأكيد مزيد من أن البيانات مخزنة في Redis، قم بإيقاف الخادم باستخدام CTRL+C. اتصل بعميل خادم Redis باستخدام الأمر التالي:

  1. redis-cli

استرجاع البيانات تحت المفتاح red-snapper:

  1. get red-snapper

ستكون مخرجاتك مشابهة لما يلي (تم تحريرها لأسباب الاختصار):

Output
"[{\"Fishery Management\":\"<ul>\\n<li><a...3\"}]"

تُظهر المخرجات النسخة المُسلسلة للبيانات JSON التي يقوم API بإرجاعها عند زيارة نقطة النهاية /fish/red-snapper، مما يؤكد أن بيانات الـ API مخزنة في ذاكرة التخزين المؤقت Redis.

اخرج من عميل خادم Redis:

  1. exit

الآن بما أنه يمكنك تخزين البيانات من API، يمكنك أيضًا تعيين صلاحية الذاكرة المؤقتة.

الخطوة 4 — تنفيذ صلاحية الذاكرة المؤقتة

عند تخزين البيانات المؤقتة، تحتاج إلى معرفة مدى تغيير البيانات. بعض بيانات واجهة برمجة التطبيقات تتغير في دقائق؛ بينما تتغير البيانات الأخرى في ساعات، أسابيع، أشهر، أو سنوات. ضبط مدة التخزين المؤقت المناسبة يضمن أن تقدم تطبيقك بيانات محدثة لمستخدميك.

في هذه الخطوة، ستقوم بتحديد صلاحية التخزين المؤقت لبيانات واجهة برمجة التطبيقات التي تحتاج إلى تخزينها في ريديس. عند انتهاء صلاحية الذاكرة المؤقتة، سيُرسَل تطبيقك طلبًا إلى واجهة برمجة التطبيقات لاسترداد البيانات الأخيرة.

تحتاج إلى الرجوع إلى وثائق واجهة برمجة التطبيقات الخاصة بك لتحديد الوقت الصحيح لانتهاء صلاحية التخزين المؤقت. ستذكر معظم الوثائق مدى تحديث البيانات بانتظام. ومع ذلك، هناك بعض الحالات حيث لا تقدم الوثائق هذه المعلومات، لذا قد تضطر إلى التخمين. يمكن أن يُظهر فحص خاصية last_updated في نقاط نهاية واجهة برمجة التطبيقات المختلفة كيفية تحديث البيانات بانتظام.

بمجرد اختيارك لمدة التخزين المؤقت، عليك تحويلها إلى ثواني. لغرض التوضيح في هذا البرنامج التعليمي، ستقوم بتعيين مدة التخزين المؤقت إلى 3 دقائق أو 180 ثانية. ستجعل هذه المدة العينية عملية اختبار وظيفة مدة التخزين المؤقتة أسهل.

لتنفيذ مدة صلاحية التخزين المؤقت، افتح ملف server.js:

  1. nano server.js

أضف الكود المظلل:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  ...
})();

async function fetchApiData(species) {
  ...
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results), {
        EX: 180,
        NX: true,
      });
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

في طريقة set() في وحدة node-redis، قم بتمرير وسيطًا ثالثًا من كائن بالخصائص التالية:

  • EX: يقبل قيمة بمدة التخزين المؤقت بالثواني.
  • NX: عند تعيينها على true، فإنها تضمن أن طريقة set() يجب أن تقوم فقط بتعيين مفتاح لا يوجد بالفعل في ريديس.

احفظ وأغلق ملفك.

العودة إلى عميل خادم Redis لاختبار صلاحية التخزين المؤقت:

  1. redis-cli

حذف المفتاح red-snapper في Redis:

  1. del red-snapper

الخروج من عميل Redis:

  1. exit

الآن، قم ببدء خادم التطوير باستخدام أمر node:

  1. node server.js

انتقل إلى متصفحك وقم بتحديث عنوان http://localhost:3000/fish/red-snapper. لمدة ثلاث دقائق قادمة، إذا قمت بتحديث العنوان، يجب أن يكون الإخراج في الطرفية متسقًا مع الإخراج التالي:

Output
App listening on port 3000 Request sent to the API

بعد مرور ثلاث دقائق، قم بتحديث العنوان في متصفحك. في الطرفية، يجب أن ترى أن “تم إرسال الطلب إلى الواجهة البرمجية” قد تم تسجيله مرتين.

Output
App listening on port 3000 Request sent to the API Request sent to the API

يوضح هذا الإخراج أن التخزين المؤقت قد انتهت صلاحيته، وتم إرسال طلب إلى الواجهة البرمجية مرة أخرى.

يمكنك إيقاف خادم Express الآن.

الآن بعد أن يمكنك تحديد صلاحية التخزين المؤقت، ستقوم بتخزين البيانات باستخدام middleware التالي.

الخطوة 5 — تخزين البيانات في middleware

في هذه الخطوة، ستستخدم middleware Express لتخزين البيانات. Middleware هو وظيفة يمكنها الوصول إلى كائن الطلب، كائن الاستجابة، ودالة استدعاء يجب تشغيلها بعد تنفيذه. الوظيفة التي تشغل بعد middleware لديها أيضًا الوصول إلى كائنات الطلب والاستجابة. عند استخدام middleware، يمكنك تعديل كائنات الطلب والاستجابة أو إرجاع استجابة للمستخدم في وقت سابق.

لاستخدام الوسيطة في تطبيقك للتخزين المؤقت، ستقوم بتعديل دالة المعالج getSpeciesData() لاسترجاع البيانات من واجهة برمجة التطبيقات وتخزينها في ريديس. ستقوم بنقل جميع الشفرة التي تبحث عن البيانات في ريديس إلى دالة الوسيطة cacheData.

عند زيارة نقطة النهاية /fish/:species، ستقوم دالة الوسيطة بالتشغيل أولاً للبحث عن البيانات في التخزين المؤقت؛ إذا تم العثور عليها، ستعيد استجابة، ولن يتم تشغيل دالة getSpeciesData. ومع ذلك، إذا لم تجد الوسيطة البيانات في التخزين المؤقت، فسوف تستدعي دالة getSpeciesData لاسترجاع البيانات من واجهة برمجة التطبيقات وتخزينها في ريديس.

أولاً، افتح ملف server.js:

  1. nano server.js

ثم، احذف الشفرة المظللة:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results), {
        EX: 180,
        NX: true,
      });
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

في دالة getSpeciesData()، قم بإزالة جميع الشفرة التي تبحث عن البيانات المخزنة في ريديس. كما قم بإزالة المتغير isCached حيث أن دالة getSpeciesData() ستقوم فقط بالحصول على البيانات من واجهة برمجة التطبيقات وتخزينها في ريديس.

بمجرد أن يتم إزالة الشفرة، قم بتعيين fromCache إلى false كما هو مظلل أدناه، بحيث تبدو دالة getSpeciesData() كما يلي:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    await redisClient.set(species, JSON.stringify(results), {
      EX: 180,
      NX: true,
    });

    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

تقوم دالة getSpeciesData() بإسترجاع البيانات من واجهة برمجة التطبيقات، تخزينها في التخزين المؤقت، وتعيد استجابة للمستخدم.

بعد ذلك، أضف الشفرة التالية لتعريف دالة الوسيطة لتخزين البيانات في ريديس:

fish_wiki/server.js
...
async function cacheData(req, res, next) {
  const species = req.params.species;
  let results;
  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      results = JSON.parse(cacheResults);
      res.send({
        fromCache: true,
        data: results,
      });
    } else {
      next();
    }
  } catch (error) {
    console.error(error);
    res.status(404);
  }
}

async function getSpeciesData(req, res) {
...
}
...

تأخذ وظيفة middleware cacheData() ثلاثة مُعاملات: req و res و next. في الكتلة try، تقوم الوظيفة بالتحقق مما إذا كانت القيمة في متغير species لديها بيانات مخزنة في Redis تحت مفتاحها. إذا كانت البيانات موجودة في Redis، يتم إرجاعها وتعيينها إلى متغير cacheResults.

بعد ذلك، تقوم البيانات بالتحقق في البيانات إذا كانت موجودة. يتم حفظ البيانات في المتغير results إذا كانت القيمة صحيحة. بعد ذلك، يستخدم الوسيطة الوسيطة الوظيفة send() لإرجاع كائن بالخصائص fromCache المعينة على true و data المعينة على المتغير results.

ومع ذلك، إذا كانت قيمة الكائن if تقوم بالتقييم على أنها خاطئة، يتم تحويل التنفيذ إلى الكتلة else. ضمن الكتلة else، تقوم بالاتصال بـ next()، الذي يمر بالتحكم إلى الوظيفة التالية التي يجب تنفيذها بعد ذلك.

لجعل وظيفة cacheData() للوسيطة تمرر التحكم إلى الوظيفة getSpeciesData() عند استدعاء next()، قم بتحديث الطريقة get() في وحدة express وفقًا لذلك:

fish_wiki/server.js
...
app.get("/fish/:species", cacheData, getSpeciesData);
...

الآن تأخذ الطريقة get() cacheData كمُعامل ثاني، وهي الوسيطة التي تبحث عن البيانات المخزنة في Redis وتُرجع استجابة عند العثور عليها.

الآن، عند زيارة النقطة النهائية /fish/:species، يُنفَّذ cacheData() أولاً. إذا تمت مضاعفة البيانات، سيُرجَع الاستجابة، ويُنتهي دورة الطلب والاستجابة هنا. ومع ذلك، إذا لم يتم العثور على بيانات في المخزن، سيتم استدعاء getSpeciesData() لاسترجاع البيانات من الواجهة البرمجية، وتخزينها في المخزن، وإرجاع استجابة.

الملف الكامل سيبدو الآن هكذا:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function cacheData(req, res, next) {
  const species = req.params.species;
  let results;
  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      results = JSON.parse(cacheResults);
      res.send({
        fromCache: true,
        data: results,
      });
    } else {
      next();
    }
  } catch (error) {
    console.error(error);
    res.status(404);
  }
}
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    await redisClient.set(species, JSON.stringify(results), {
      EX: 180,
      NX: true,
    });

    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", cacheData, getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

احفظ وأخرج من الملف.

للاختبار الصحيح للتخزين المؤقت، يمكنك حذف مفتاح red-snapper في Redis. للقيام بذلك، انتقل إلى عميل Redis:

  1. redis-cli

أزل مفتاح red-snapper:

  1. del red-snapper

اخرج من عميل Redis:

  1. exit

الآن، قم بتشغيل ملف server.js:

  1. node server.js

بمجرد بدء تشغيل الخادم، عد إلى المتصفح وقم بزيارة http://localhost:3000/fish/red-snapper مرة أخرى. قم بتحديثه مرات عديدة.

ستُسجَّل رسالة في الطرفية تفيد بإرسال طلب إلى الواجهة البرمجية. سيخدم الوسيط cacheData() جميع الطلبات للدقائق الثلاث القادمة. ستبدو النتيجة مشابهة لهذا إذا قمت بتحديث عنوان URL بشكل عشوائي في فترة زمنية تبلغ أربع دقائق:

Output
App listening on port 3000 Request sent to the API Request sent to the API

السلوك متسق مع كيفية عمل التطبيق في القسم السابق.

يمكنك الآن تخزين البيانات في Redis باستخدام الوسيط.

الختام

في هذا المقال، قمت ببناء تطبيق يقوم بجلب البيانات من واجهة برمجة التطبيقات (API) ويقوم بإرجاع البيانات كاستجابة للعميل. ثم قمت بتعديل التطبيق لتخزين استجابة API في Redis في الزيارة الأولى وتقديم البيانات من الذاكرة المؤقتة لجميع الطلبات التالية. ثم قمت بتعديل مدة التخزين المؤقت لتنتهي بعد مرور مدة معينة من الزمن، ومن ثم استخدمت الوسيطة للتعامل مع استرجاع البيانات المخزنة مؤقتًا.

كخطوة مقبلة، يمكنك استكشاف وثائق Node Redis لتعلم المزيد حول الميزات المتاحة في وحدة node-redis. يمكنك أيضًا قراءة وثائق Axios و Express للنظر العميق في المواضيع المغطاة في هذا البرنامج التعليمي.

لمواصلة بناء مهارتك في Node.js، انظر سلسلة كيفية البرمجة في Node.js.

Source:
https://www.digitalocean.com/community/tutorials/how-to-implement-caching-in-node-js-using-redis