1. نظرة عامة
الهدف من هذا الدرس التطبيقي حول الترميز هو اكتساب خبرة في استخدام خدمات "الحوسبة بدون خادم" التي تقدّمها Google Cloud Platform:
- Cloud Functions: لنشر وحدات صغيرة من منطق النشاط التجاري في شكل دوال تتفاعل مع أحداث مختلفة (رسائل Pub/Sub وملفات جديدة في Cloud Storage وطلبات HTTP وغير ذلك)
- App Engine: لنشر تطبيقات الويب وواجهات برمجة تطبيقات الويب وأنظمة الخلفية للأجهزة الجوّالة والأصول الثابتة وتقديمها، مع إمكانات التوسّع السريع للأعلى والأسفل
- Cloud Run: لنشر الحاويات وتوسيع نطاقها، ويمكن أن تحتوي على أي لغة أو وقت تشغيل أو مكتبة
يمكنك أيضًا التعرّف على كيفية الاستفادة من هذه الخدمات التي لا تتطلّب خادمًا لنشر واجهات برمجة التطبيقات على الويب وREST وتوسيع نطاقها، بالإضافة إلى التعرّف على بعض مبادئ التصميم الجيدة المتوافقة مع REST.
في ورشة العمل هذه، سننشئ أداة استكشاف رفوف الكتب تتألف من:
- دالة Cloud Function: لاستيراد مجموعة البيانات الأولية للكتب المتوفرة في مكتبتنا، في قاعدة بيانات المستندات Cloud Firestore
- حاوية Cloud Run: ستعرض واجهة REST API على محتوى قاعدة البيانات،
- واجهة أمامية على الويب في App Engine: لتصفُّح قائمة الكتب من خلال طلب بيانات من واجهة REST API.
إليك الشكل الذي ستبدو عليه واجهة الويب الأمامية في نهاية هذا الدرس التطبيقي حول الترميز:

ما ستتعلمه
- وظائف السحابة الإلكترونية
- Cloud Firestore
- Cloud Run
- App Engine
2. الإعداد والمتطلبات
إعداد البيئة بالسرعة التي تناسبك
- سجِّل الدخول إلى Google Cloud Console وأنشِئ مشروعًا جديدًا أو أعِد استخدام مشروع حالي. إذا لم يكن لديك حساب على Gmail أو Google Workspace، عليك إنشاء حساب.



- اسم المشروع هو الاسم المعروض للمشاركين في هذا المشروع. وهي سلسلة أحرف لا تستخدمها Google APIs. ويمكنك تعديلها في أي وقت.
- رقم تعريف المشروع هو معرّف فريد في جميع مشاريع Google Cloud ولا يمكن تغييره (لا يمكن تغييره بعد ضبطه). تنشئ Cloud Console سلسلة فريدة تلقائيًا، ولا يهمّك عادةً ما هي. في معظم دروس البرمجة، عليك الرجوع إلى رقم تعريف مشروعك (يُشار إليه عادةً باسم
PROJECT_ID). إذا لم يعجبك رقم التعريف الذي تم إنشاؤه، يمكنك إنشاء رقم آخر عشوائي. يمكنك بدلاً من ذلك تجربة اسم مستخدم من اختيارك ومعرفة ما إذا كان متاحًا. لا يمكن تغيير هذا الخيار بعد هذه الخطوة ويظل ساريًا طوال مدة المشروع. - للعلم، هناك قيمة ثالثة، وهي رقم المشروع، وتستخدمها بعض واجهات برمجة التطبيقات. يمكنك الاطّلاع على مزيد من المعلومات حول هذه القيم الثلاث في المستندات.
- بعد ذلك، عليك تفعيل الفوترة في Cloud Console لاستخدام موارد/واجهات برمجة تطبيقات Cloud. لن تكلفك تجربة هذا الدرس البرمجي الكثير، إن وُجدت أي تكلفة. لإيقاف الموارد وتجنُّب تحمّل تكاليف فوترة تتجاوز هذا البرنامج التعليمي، يمكنك حذف الموارد التي أنشأتها أو حذف المشروع. يمكن لمستخدمي Google Cloud الجدد الاستفادة من برنامج الفترة التجريبية المجانية بقيمة 300 دولار أمريكي.
بدء Cloud Shell
على الرغم من إمكانية تشغيل Google Cloud عن بُعد من الكمبيوتر المحمول، ستستخدم في هذا الدرس التطبيقي حول الترميز Google Cloud Shell، وهي بيئة سطر أوامر تعمل في السحابة الإلكترونية.
من Google Cloud Console، انقر على رمز Cloud Shell في شريط الأدوات أعلى يسار الصفحة:

لن يستغرق توفير البيئة والاتصال بها سوى بضع لحظات. عند الانتهاء، من المفترض أن يظهر لك ما يلي:

يتم تحميل هذه الآلة الافتراضية مزوّدة بكل أدوات التطوير التي ستحتاج إليها. توفّر هذه الخدمة دليلًا منزليًا دائمًا بسعة 5 غيغابايت، وتعمل على Google Cloud، ما يؤدي إلى تحسين أداء الشبكة والمصادقة بشكل كبير. يمكن إكمال جميع المهام في هذا الدرس العملي ضمن المتصفّح. لست بحاجة إلى تثبيت أي تطبيق.
3- إعداد البيئة وتفعيل واجهات برمجة التطبيقات على السحابة الإلكترونية
لاستخدام الخدمات المختلفة التي سنحتاج إليها خلال هذا المشروع، سنفعّل بعض واجهات برمجة التطبيقات. سننفّذ ذلك من خلال تشغيل الأمر التالي في Cloud Shell:
$ gcloud services enable \
appengine.googleapis.com \
cloudbuild.googleapis.com \
cloudfunctions.googleapis.com \
compute.googleapis.com \
firestore.googleapis.com \
run.googleapis.com
بعد مرور بعض الوقت، من المفترض أن تظهر لك رسالة تفيد بإتمام العملية بنجاح:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
سنُعدّ أيضًا متغيّر بيئة سنحتاج إليه على طول الطريق: منطقة السحابة الإلكترونية التي سننشر فيها الدالة والتطبيق والحاوية:
$ export REGION=europe-west3
بما أنّنا سنخزّن البيانات في قاعدة بيانات Cloud Firestore، علينا إنشاء قاعدة البيانات:
$ gcloud app create --region=${REGION}
$ gcloud firestore databases create --location=${REGION}
في وقت لاحق من هذا الدرس التطبيقي حول الترميز، وعند تنفيذ واجهة REST API، سنحتاج إلى ترتيب البيانات وتصفيتها. لهذا الغرض، سننشئ ثلاثة فهارس:
$ gcloud firestore indexes composite create --collection-group=books \
--field-config field-path=language,order=ascending \
--field-config field-path=updated,order=descending
$ gcloud firestore indexes composite create --collection-group=books \
--field-config field-path=author,order=ascending \
--field-config field-path=updated,order=descending
تتطابق الفهارس الثلاثة مع عمليات البحث التي سنجريها حسب المؤلف أو اللغة، مع الحفاظ على الترتيب في المجموعة من خلال حقل معدَّل.
4. الحصول على الشفرة
احصل على الرمز من مستودع Github التالي:
$ git clone https://github.com/glaforge/serverless-web-apis
تتم كتابة الرمز البرمجي للتطبيق باستخدام Node.JS.
ستتوفّر لك بنية المجلدات التالية ذات الصلة بهذا الدرس التطبيقي:
serverless-web-apis
|
├── data
| ├── books.json
|
├── function-import
| ├── index.js
| ├── package.json
|
├── run-crud
| ├── index.js
| ├── package.json
| ├── Dockerfile
|
├── appengine-frontend
├── public
| ├── css/style.css
| ├── html/index.html
| ├── js/app.js
├── index.js
├── package.json
├── app.yaml
في ما يلي المجلدات ذات الصلة:
data: يحتوي هذا المجلد على عينة بيانات لقائمة تضم 100 كتاب.function-import: ستوفّر هذه الدالة نقطة نهاية لاستيراد بيانات نموذجية.run-crud: سيعرض هذا الحاوي واجهة برمجة تطبيقات على الويب للوصول إلى بيانات الكتب المخزّنة في Cloud Firestore.appengine-frontend: سيعرض تطبيق الويب هذا على App Engine واجهة أمامية بسيطة للقراءة فقط لتصفّح قائمة الكتب.
5- عيّنة من بيانات مكتبة الكتب
في مجلد البيانات، لدينا ملف books.json يحتوي على قائمة بمئة كتاب، ربما يستحق القراءة. مستند JSON هذا هو عبارة عن مصفوفة تحتوي على عناصر JSON. لنلقِ نظرة على شكل البيانات التي سنستوعبها من خلال Cloud Function:
[
{
"isbn": "9780435272463",
"author": "Chinua Achebe",
"language": "English",
"pages": 209,
"title": "Things Fall Apart",
"year": 1958
},
{
"isbn": "9781414251196",
"author": "Hans Christian Andersen",
"language": "Danish",
"pages": 784,
"title": "Fairy tales",
"year": 1836
},
...
]
تحتوي جميع إدخالات الكتب في هذه المصفوفة على المعلومات التالية:
isbn: رمز ISBN-13 الذي يحدّد الكتاب.-
author: تمثّل هذه السمة اسم مؤلِّف الكتاب. language: اللغة المنطوقة التي كُتب بها الكتاب-
pages: عدد صفحات الكتاب - استبدِل
titleبعنوان الكتاب. year: تمثّل السنة التي تم فيها نشر الكتاب.
6. نقطة نهاية دالة لاستيراد بيانات نموذجية للكتب
في هذا القسم الأول، سننفّذ نقطة النهاية التي سيتم استخدامها لاستيراد بيانات الكتب النموذجية. سنستخدم Cloud Functions لهذا الغرض.
استكشاف الرمز
لنبدأ بالاطّلاع على الملف package.json:
{
"name": "function-import",
"description": "Import sample book data",
"license": "Apache-2.0",
"dependencies": {
"@google-cloud/firestore": "^4.9.9"
},
"devDependencies": {
"@google-cloud/functions-framework": "^3.1.0"
},
"scripts": {
"start": "npx @google-cloud/functions-framework --target=parseBooks"
}
}
في التبعيات في وقت التشغيل، نحتاج فقط إلى وحدة @google-cloud/firestore NPM للوصول إلى قاعدة البيانات وتخزين بيانات الكتب. في الخلفية، يوفّر وقت تشغيل Cloud Functions أيضًا إطار عمل الويب Express، لذلك لسنا بحاجة إلى تعريفه كعنصر تابع.
في تبعيات التطوير، نحدّد Functions Framework (@google-cloud/functions-framework)، وهو إطار وقت التشغيل المستخدَم لاستدعاء الدوال. وهو إطار عمل مفتوح المصدر يمكنك استخدامه أيضًا على جهازك (في حالتنا، داخل Cloud Shell) لتشغيل الدوال بدون الحاجة إلى نشرها في كل مرة تجري فيها تغييرًا، ما يؤدي إلى تحسين حلقة ملاحظات التطوير.
لتثبيت الموارد الاعتمادية، استخدِم الأمر install:
$ npm install
يستخدم النص البرمجي start Functions Framework لمنحك أمرًا يمكنك استخدامه لتشغيل الدالة على الجهاز باتّباع التعليمات التالية:
$ npm start
يمكنك استخدام curl أو ربما معاينة الويب في Cloud Shell لطلبات HTTP GET للتفاعل مع الدالة.
لنلقِ نظرة الآن على ملف index.js الذي يحتوي على منطق دالة استيراد بيانات الكتب:
const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');
ننشئ مثيلاً لوحدة Firestore، ونشير إلى مجموعة الكتب (على غرار جدول في قواعد البيانات الارتباطية).
functions.http('parseBooks', async (req, resp) => {
if (req.method !== "POST") {
resp.status(405).send({error: "Only method POST allowed"});
return;
}
if (req.headers['content-type'] !== "application/json") {
resp.status(406).send({error: "Only application/json accepted"});
return;
}
...
})
نحن بصدد تصدير دالة parseBooks JavaScript. هذه هي الدالة التي سنعلن عنها عند نشرها لاحقًا.
تتحقّق التعليمات التالية مما يلي:
- نحن نقبل فقط طلبات HTTP
POST، وإلا سنعرض رمز الحالة405للإشارة إلى أنّه لا يُسمح باستخدام طرق HTTP الأخرى. - نحن نقبل فقط حمولات
application/json، وإلا سنرسل رمز الحالة406للإشارة إلى أنّ هذا التنسيق غير مقبول للحمولة.
const books = req.body;
const writeBatch = firestore.batch();
for (const book of books) {
const doc = bookStore.doc(book.isbn);
writeBatch.set(doc, {
title: book.title,
author: book.author,
language: book.language,
pages: book.pages,
year: book.year,
updated: Firestore.Timestamp.now()
});
}
بعد ذلك، يمكننا استرداد حمولة JSON من خلال body للطلب. نحن بصدد إعداد عملية دفعية في Firestore لتخزين جميع الكتب بشكل مجمّع. نكرّر مصفوفة JSON التي تتألف من تفاصيل الكتاب، وننتقل إلى الحقول isbn وtitle وauthor وlanguage وpages وyear. سيكون رمز ISBN الخاص بالكتاب هو المفتاح الأساسي أو المعرّف.
try {
await writeBatch.commit();
console.log("Saved books in Firestore");
} catch (e) {
console.error("Error saving books:", e);
resp.status(400).send({error: "Error saving books"});
return;
};
resp.status(202).send({status: "OK"});
بعد أن أصبحت معظم البيانات جاهزة، يمكننا تنفيذ العملية. إذا تعذّرت عملية التخزين، سنعرض رمز الحالة 400 للإشارة إلى تعذّرها. في ما عدا ذلك، يمكننا عرض استجابة OK، مع رمز الحالة 202 الذي يشير إلى أنّه تم قبول طلب الحفظ المجمّع.
تشغيل دالة الاستيراد واختبارها
قبل تشغيل الرمز، سنثبّت التبعيات باستخدام:
$ npm install
لتشغيل الدالة محليًا، سنستخدم أمر البرنامج النصي start الذي حدّدناه في package.json، وذلك بفضل Functions Framework:
$ npm start > start > npx @google-cloud/functions-framework --target=parseBooks Serving function... Function: parseBooks URL: http://localhost:8080/
لإرسال طلب HTTP POST إلى الدالة المحلية، يمكنك تنفيذ ما يلي:
$ curl -d "@../data/books.json" \
-H "Content-Type: application/json" \
http://localhost:8080/
عند تشغيل هذا الأمر، ستظهر لك النتيجة التالية التي تؤكّد أنّ الدالة تعمل على الجهاز المحلي:
{"status":"OK"}
يمكنك أيضًا الانتقال إلى واجهة مستخدم Cloud Console للتأكّد من أنّ البيانات مخزَّنة في Firestore:

في لقطة الشاشة أعلاه، يمكننا رؤية مجموعة books التي تم إنشاؤها، وقائمة مستندات الكتب المحدّدة برمز ISBN الخاص بالكتاب، وتفاصيل إدخال هذا الكتاب المحدّد على اليسار.
نشر الدالة في السحابة الإلكترونية
لنشر الدالة في Cloud Functions، سنستخدم الأمر التالي في الدليل function-import:
$ gcloud functions deploy bulk-import \
--gen2 \
--trigger-http \
--runtime=nodejs20 \
--allow-unauthenticated \
--max-instances=30
--region=${REGION} \
--source=. \
--entry-point=parseBooks
ننشر الدالة بالاسم الرمزي bulk-import. يتم تشغيل هذه الدالة من خلال طلبات HTTP. نستخدم وقت التشغيل Node.JS 20. ننشر الدالة بشكل علني (من المفترض أن نؤمّن نقطة النهاية هذه). نحدّد المنطقة التي نريد أن تقيم فيها الدالة. ونشير إلى المصادر في الدليل المحلي ونستخدم parseBooks (دالة JavaScript التي تم تصديرها) كنقطة دخول.
بعد دقيقتَين أو أقل، يتم نشر الدالة في السحابة الإلكترونية. في واجهة مستخدم Cloud Console، من المفترض أن تظهر الدالة:

في ناتج النشر، من المفترض أن تتمكّن من رؤية عنوان URL الخاص بالدالة، والذي يتّبع اصطلاح تسمية معيّنًا (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME})، وبالطبع، يمكنك أيضًا العثور على عنوان URL هذا لتشغيل HTTP في واجهة مستخدم Cloud Console، في علامة التبويب "المشغّل":

يمكنك أيضًا استرداد عنوان URL من خلال سطر الأوامر باستخدام gcloud:
$ export BULK_IMPORT_URL=$(gcloud functions describe bulk-import \
--region=$REGION \
--format 'value(httpsTrigger.url)')
$ echo $BULK_IMPORT_URL
لنخزِّنها في متغيّر البيئة BULK_IMPORT_URL، حتى نتمكّن من إعادة استخدامها لاختبار الدالة التي تم نشرها.
اختبار الدالة التي تم نشرها
باستخدام أمر curl مشابه للأمر الذي استخدمناه سابقًا لاختبار تشغيل الدالة محليًا، سنختبر الدالة التي تم نشرها. التغيير الوحيد الذي سيتم إجراؤه هو عنوان URL:
$ curl -d "@../data/books.json" \
-H "Content-Type: application/json" \
$BULK_IMPORT_URL
مرة أخرى، في حال نجاح العملية، من المفترض أن تعرض النتيجة التالية:
{"status":"OK"}
بعد نشر دالة الاستيراد وإعدادها، وبعد تحميل عينة البيانات، حان الوقت لتطوير واجهة REST API التي تعرض مجموعة البيانات هذه.
7. عقد REST API
على الرغم من أنّنا لن نحدّد عقد واجهة برمجة التطبيقات باستخدام، على سبيل المثال، مواصفات Open API، سنلقي نظرة على نقاط النهاية المختلفة لواجهة REST API.
تتبادل واجهة برمجة التطبيقات عناصر JSON الخاصة بالكتب، والتي تتألف من:
-
isbn(اختياري):Stringيتكوّن من 13 حرفًا ويمثّل رمز ISBN صالحًا -
author—Stringغير فارغ يمثّل اسم مؤلف الكتاب language:Stringغير فارغ يحتوي على اللغة التي كُتب بها الكتابpages:Integerموجب يمثّل عدد صفحات الكتابtitle—Stringغير فارغ يتضمّن عنوان الكتابyear: قيمةIntegerلسنة نشر الكتاب
مثال على حمولة كتاب:
{
"isbn": "9780435272463",
"author": "Chinua Achebe",
"language": "English",
"pages": 209,
"title": "Things Fall Apart",
"year": 1958
}
GET /books
يمكنك الحصول على قائمة بجميع الكتب، مع إمكانية فلترتها حسب المؤلّف و/أو اللغة، وتقسيمها إلى صفحات تضم 10 نتائج في كل مرة.
حمولة نص الطلب: لا شيء.
مَعلمات طلب البحث:
-
author(اختياري): لفلترة قائمة الكتب حسب المؤلف -
language(اختياري): يفلتر قائمة الكتب حسب اللغة. -
page(اختياري، القيمة التلقائية = 0): يشير إلى ترتيب صفحة النتائج المطلوب عرضها.
تعرض هذه الطريقة مصفوفة JSON تتضمّن عناصر الكتب.
رموز الحالة:
-
200— عندما ينجح الطلب في استرجاع قائمة الكتب -
400: في حال حدوث خطأ
POST /books وPOST /books/{isbn}
إرسال حمولة كتاب جديدة، إما باستخدام مَعلمة مسار isbn (في هذه الحالة، لا يلزم تضمين الرمز isbn في حمولة الكتاب) أو بدونها (في هذه الحالة، يجب تضمين الرمز isbn في حمولة الكتاب)
حمولة نص الطلب: عنصر كتاب
مَعلمات طلب البحث: لا يوجد
لا تعرض هذه الدالة أي قيمة.
رموز الحالة:
201— عندما يتم تخزين الكتاب بنجاح406— إذا كان الرمزisbnغير صالح،-
400: في حال حدوث خطأ
GET /books/{isbn}
يستردّ هذا الإجراء كتابًا من المكتبة، ويتم تحديد الكتاب من خلال الرمز isbn الذي يتم تمريره كمَعلمة مسار.
حمولة نص الطلب: لا شيء
مَعلمات طلب البحث: لا يوجد
تعرض هذه الطريقة كائن JSON خاصًا بكتاب، أو كائن خطأ إذا لم يكن الكتاب متوفّرًا.
رموز الحالة:
200— إذا تم العثور على الكتاب في قاعدة البيانات400— في حال حدوث خطأ،-
404: إذا تعذّر العثور على الكتاب -
406: إذا كان الرمزisbnغير صالح.
PUT /books/{isbn}
تعدّل هذه الطريقة كتابًا حاليًا، يتم تحديده من خلال isbn الذي يتم تمريره كمَعلمة مسار.
حمولة نص الطلب: عنصر كتاب يمكن تمرير الحقول التي تحتاج إلى تعديل فقط، أما الحقول الأخرى فهي اختيارية.
مَعلمات طلب البحث: لا يوجد
تعرض هذه الدالة الكتاب المعدَّل.
رموز الحالة:
-
200: عند تعديل الكتاب بنجاح 400— في حال حدوث خطأ،-
406: إذا كان الرمزisbnغير صالح.
DELETE /books/{isbn}
يحذف هذا الإجراء كتابًا حاليًا، ويتم تحديد الكتاب من خلال isbn الذي يتم تمريره كمَعلمة مسار.
حمولة نص الطلب: لا شيء
مَعلمات طلب البحث: لا يوجد
لا تعرض هذه الدالة أي قيمة.
رموز الحالة:
-
204: عند حذف الكتاب بنجاح -
400: في حال حدوث خطأ
8. نشر واجهة برمجة تطبيقات REST وإتاحتها في حاوية
استكشاف الرمز
Dockerfile
لنبدأ بإلقاء نظرة على Dockerfile، الذي سيكون مسؤولاً عن وضع الرمز البرمجي لتطبيقنا في حاوية:
FROM node:20-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . ./
CMD [ "node", "index.js" ]
نحن نستخدم صورة "مختصرة" من Node.JS 20. نحن نعمل في الدليل /usr/src/app. نحن بصدد نسخ ملف package.json (التفاصيل أدناه) الذي يحدّد التبعيات، من بين أمور أخرى. نثبّت التبعيات باستخدام npm install، وننسخ الرمز البرمجي المصدر. أخيرًا، نشير إلى كيفية تشغيل هذا التطبيق باستخدام الأمر node index.js.
package.json
بعد ذلك، يمكننا إلقاء نظرة على الملف package.json:
{
"name": "run-crud",
"description": "CRUD operations over book data",
"license": "Apache-2.0",
"engines": {
"node": ">= 20.0.0"
},
"dependencies": {
"@google-cloud/firestore": "^4.9.9",
"cors": "^2.8.5",
"express": "^4.17.1",
"isbn3": "^1.1.10"
},
"scripts": {
"start": "node index.js"
}
}
نحدّد أنّنا نريد استخدام Node.JS 14، كما كان الحال مع Dockerfile.
يعتمد تطبيق واجهة برمجة التطبيقات على الويب على ما يلي:
- وحدة NPM في Firestore للوصول إلى بيانات الكتب في قاعدة البيانات
- مكتبة
corsللتعامل مع طلبات CORS (CORS)، لأنّه سيتم استدعاء واجهة REST API من رمز العميل الخاص بواجهة تطبيق الويب الأمامية على App Engine - إطار عمل Express، وهو إطار عمل الويب الذي سنستخدمه لتصميم واجهة برمجة التطبيقات
- ثم وحدة
isbn3التي تساعد في التحقّق من صحة رموز ISBN الخاصة بالكتب.
نحدّد أيضًا النص البرمجي start، الذي سيكون مفيدًا لبدء التطبيق محليًا لأغراض التطوير والاختبار.
index.js
لننتقل إلى الجزء الأساسي من الرمز، مع نظرة متعمقة على index.js:
const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');
نحتاج إلى وحدة Firestore، ونشير إلى المجموعة books التي يتم فيها تخزين بيانات الكتب.
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
const querystring = require('querystring');
const cors = require('cors');
app.use(cors({
exposedHeaders: ['Content-Length', 'Content-Type', 'Link'],
}));
نستخدم Express، وهو إطار عمل الويب، لتنفيذ واجهة REST API. نحن نستخدم الوحدة body-parser لتحليل حمولات JSON التي يتم تبادلها مع واجهة برمجة التطبيقات.
وحدة querystring مفيدة لمعالجة عناوين URL، وهذا ما سنفعله عند إنشاء عناوين Link لأغراض تقسيم المحتوى على صفحات (سنتناول المزيد من التفاصيل حول هذا الموضوع لاحقًا).
بعد ذلك، نضبط وحدة cors. نحدّد العناوين التي نريد تمريرها عبر CORS، لأنّ معظمها تتم إزالته عادةً، ولكننا نريد هنا الاحتفاظ بطول المحتوى ونوعه المعتادَين، بالإضافة إلى عنوان Link الذي سنحدّده للتقسيم على صفحات.
const ISBN = require('isbn3');
function isbnOK(isbn, res) {
const parsedIsbn = ISBN.parse(isbn);
if (!parsedIsbn) {
res.status(406)
.send({error: `Invalid ISBN: ${isbn}`});
return false;
}
return parsedIsbn;
}
سنستخدم وحدة isbn3 NPM لتحليل رموز ISBN والتحقّق من صحتها، وسنطوّر دالة مساعدة صغيرة لتحليل رموز ISBN، والردّ برمز الحالة 406 في الردّ، إذا كانت رموز ISBN غير صالحة.
GET /books
لنلقِ نظرة على نقطة النهاية GET /books جزءًا جزءًا:
app.get('/books', async (req, res) => {
try {
var query = new Firestore().collection('books');
if (!!req.query.author) {
console.log(`Filtering by author: ${req.query.author}`);
query = query.where("author", "==", req.query.author);
}
if (!!req.query.language) {
console.log(`Filtering by language: ${req.query.language}`);
query = query.where("language", "==", req.query.language);
}
const page = parseInt(req.query.page) || 0;
// - - ✄ - - ✄ - - ✄ - - ✄ - - ✄ - -
} catch (e) {
console.error('Failed to fetch books', e);
res.status(400)
.send({error: `Impossible to fetch books: ${e.message}`});
}
});
نحن بصدد الاستعداد لإجراء طلب بحث في قاعدة البيانات من خلال إعداد طلب بحث. سيعتمد طلب البحث هذا على مَعلمات طلب البحث الاختيارية، وذلك للفلترة حسب المؤلف و/أو حسب اللغة. نعرض أيضًا قائمة الكتب على شكل مجموعات من 10 كتب.
إذا حدث خطأ أثناء جلب الكتب، سنعرض خطأ مع رمز الحالة 400.
لنلقِ نظرة على الجزء المقتطع من نقطة النهاية هذه:
const snapshot = await query
.orderBy('updated', 'desc')
.limit(PAGE_SIZE)
.offset(PAGE_SIZE * page)
.get();
const books = [];
if (snapshot.empty) {
console.log('No book found');
} else {
snapshot.forEach(doc => {
const {title, author, pages, year, language, ...otherFields} = doc.data();
const book = {isbn: doc.id, title, author, pages, year, language};
books.push(book);
});
}
في القسم السابق، أجرينا فلترة حسب author وlanguage، ولكن في هذا القسم، سنرتّب قائمة الكتب حسب تاريخ آخر تعديل (يظهر آخر تعديل أولاً). وسنقسم النتائج أيضًا إلى صفحات من خلال تحديد حدّ (عدد العناصر المطلوب عرضها) وإزاحة (نقطة البداية التي سيتم منها عرض المجموعة التالية من الكتب).
ننفّذ طلب البحث، ونحصل على لقطة للبيانات، ونضع هذه النتائج في مصفوفة JavaScript سيتم عرضها في نهاية الدالة.
لننهِ شرح نقطة النهاية هذه من خلال الاطّلاع على إحدى الممارسات الجيدة: استخدام العنوان Link لتحديد روابط معرّف الموارد المنتظم (URI) للصفحات الأولى أو السابقة أو التالية أو الأخيرة من البيانات (في حالتنا، سنوفّر فقط الصفحات السابقة والتالية).
var links = {};
if (page > 0) {
const prevQuery = querystring.stringify({...req.query, page: page - 1});
links.prev = `${req.path}${prevQuery != '' ? `?${prevQuery}` : ''}`;
}
if (snapshot.docs.length === PAGE_SIZE) {
const nextQuery = querystring.stringify({...req.query, page: page + 1});
links.next = `${req.path}${nextQuery != '' ? `?${nextQuery}` : ''}`;
}
if (Object.keys(links).length > 0) {
res.links(links);
}
res.status(200).send(books);
قد تبدو هذه العملية معقّدة بعض الشيء في البداية، ولكن ما نفعله هو إضافة رابط السابق إذا لم نكن في الصفحة الأولى من البيانات. ونضيف رابط التالي إذا كانت صفحة البيانات ممتلئة (أي تحتوي على الحد الأقصى لعدد الكتب كما هو محدّد بالثابت PAGE_SIZE، على افتراض أنّ هناك صفحة أخرى ستظهر مع المزيد من البيانات). نستخدم بعد ذلك الدالة resource#links() في Express لإنشاء العنوان المناسب باستخدام بنية الجملة الصحيحة.
للعلم، سيبدو عنوان الرابط على النحو التالي:
link: </books?page=1>; rel="prev", </books?page=3>; rel="next"
-
POST /booksوPOST /books/:isbn
يتم استخدام كلتا نقطتَي النهاية لإنشاء كتاب جديد. يتم تمرير رمز ISBN في حمولة الكتاب، بينما يتم تمريره في الآخر كمعلَمة مسار. في كلتا الحالتين، سيتم استدعاء الدالة createBook():
async function createBook(isbn, req, res) {
const parsedIsbn = isbnOK(isbn, res);
if (!parsedIsbn) return;
const {title, author, pages, year, language} = req.body;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.set({
title, author, pages, year, language,
updated: Firestore.Timestamp.now()
});
console.log(`Saved book ${parsedIsbn.isbn13}`);
res.status(201)
.location(`/books/${parsedIsbn.isbn13}`)
.send({status: `Book ${parsedIsbn.isbn13} created`});
} catch (e) {
console.error(`Failed to save book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to create book ${parsedIsbn.isbn13}: ${e.message}`});
}
}
نتأكّد من أنّ الرمز isbn صالح، وإلا سنخرج من الدالة (ونضبط رمز الحالة 406). نستردّ حقول الكتاب من الحمولة التي تم تمريرها في نص الطلب. بعد ذلك، سنخزّن تفاصيل الكتاب في Firestore. يتم عرض 201 عند النجاح، و400 عند التعذّر.
عند إرجاع الردّ بنجاح، نضبط أيضًا عنوان الموقع الجغرافي، وذلك لتقديم إشارات إلى عميل واجهة برمجة التطبيقات بشأن مكان المرجع الذي تم إنشاؤه حديثًا. سيكون العنوان على النحو التالي:
Location: /books/9781234567898
GET /books/:isbn
لنستردّ كتابًا محدّدًا برقم ISBN من Firestore.
app.get('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
const docSnapshot = await docRef.get();
if (!docSnapshot.exists) {
console.log(`Book not found ${parsedIsbn.isbn13}`)
res.status(404)
.send({error: `Could not find book ${parsedIsbn.isbn13}`});
return;
}
console.log(`Fetched book ${parsedIsbn.isbn13}`, docSnapshot.data());
const {title, author, pages, year, language, ...otherFields} = docSnapshot.data();
const book = {isbn: parsedIsbn.isbn13, title, author, pages, year, language};
res.status(200).send(book);
} catch (e) {
console.error(`Failed to fetch book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to fetch book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
وكالعادة، نتحقّق من صلاحية رقم ISBN. نرسل طلب بحث إلى Firestore لاسترداد الكتاب. تكون السمة snapshot.exists مفيدة لمعرفة ما إذا تم العثور على كتاب بالفعل. وفي حال عدم توفّرها، نرسل رسالة خطأ ورمز الحالة 404 Not Found. نسترد بيانات الكتاب، وننشئ عنصر JSON يمثّل الكتاب، ليتم عرضه.
PUT /books/:isbn
نستخدم طريقة PUT لتعديل كتاب حالي.
app.put('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.set({
...req.body,
updated: Firestore.Timestamp.now()
}, {merge: true});
console.log(`Updated book ${parsedIsbn.isbn13}`);
res.status(201)
.location(`/books/${parsedIsbn.isbn13}`)
.send({status: `Book ${parsedIsbn.isbn13} updated`});
} catch (e) {
console.error(`Failed to update book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to update book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
نعدّل حقل updated التاريخ/الوقت لتذكُّر آخر مرة عدّلنا فيها هذا السجلّ. نستخدم استراتيجية {merge:true} التي تستبدل الحقول الحالية بقيمها الجديدة (وإلا ستتم إزالة جميع الحقول، وسيتم حفظ الحقول الجديدة فقط في الحمولة، ما يؤدي إلى محو الحقول الحالية من التعديل السابق أو الإنشاء الأولي).
نضبط أيضًا العنوان Location للإشارة إلى معرّف الموارد المنتظم (URI) الخاص بالكتاب.
DELETE /books/:isbn
حذف الكتب عملية بسيطة. ما عليك سوى استدعاء الطريقة delete() في مرجع المستند. نعرض رمز الحالة 204 لأنّنا لا نعرض أي محتوى.
app.delete('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.delete();
console.log(`Book ${parsedIsbn.isbn13} was deleted`);
res.status(204).end();
} catch (e) {
console.error(`Failed to delete book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to delete book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
بدء خادم Express / Node
أخيرًا وليس آخرًا، نبدأ الخادم، الذي يتلقى بيانات عبر المنفذ 8080 تلقائيًا:
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Books Web API service: listening on port ${port}`);
console.log(`Node ${process.version}`);
});
تشغيل التطبيق محليًا
لتشغيل التطبيق على الجهاز، علينا أولاً تثبيت التبعيات باستخدام الأمر التالي:
$ npm install
ويمكننا بعد ذلك البدء بما يلي:
$ npm start
سيبدأ الخادم في localhost وسيستمع إلى المنفذ 8080 تلقائيًا.
يمكنك أيضًا إنشاء حاوية Docker وتشغيل صورة الحاوية باستخدام الأوامر التالية:
$ docker build -t crud-web-api . $ docker run --rm -p 8080:8080 -it crud-web-api
يُعدّ التشغيل داخل Docker أيضًا طريقة رائعة للتأكّد من أنّ عملية وضع التطبيق في حاوية ستتم بشكل جيد أثناء إنشائه على السحابة الإلكترونية باستخدام Cloud Build.
اختبار واجهة برمجة التطبيقات
بغض النظر عن طريقة تشغيل رمز REST API (مباشرةً من خلال Node أو من خلال صورة حاوية Docker)، يمكننا الآن تنفيذ بعض طلبات البحث عليه.
- إنشاء كتاب جديد (رقم ISBN في حمولة نص الطلب):
$ curl -XPOST -d '{"isbn":"9782070368228","title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
-H "Content-Type: application/json" \
http://localhost:8080/books
- إنشاء كتاب جديد (رقم ISBN في مَعلمة مسار):
$ curl -XPOST -d '{"title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
-H "Content-Type: application/json" \
http://localhost:8080/books/9782070368228
- احذف كتابًا (الكتاب الذي أنشأناه):
$ curl -XDELETE http://localhost:8080/books/9782070368228
- استرداد كتاب حسب رقم ISBN:
$ curl http://localhost:8080/books/9780140449136 $ curl http://localhost:8080/books/9782070360536
- عدِّل كتابًا حاليًا عن طريق تغيير عنوانه فقط:
$ curl -XPUT \
-d '{"title":"Book"}' \
-H "Content-Type: application/json" \
http://localhost:8080/books/9780003701203
- استرداد قائمة الكتب (أول 10 كتب):
$ curl http://localhost:8080/books
- للعثور على الكتب التي كتبها مؤلف معيّن، اتّبِع الخطوات التالية:
$ curl http://localhost:8080/books?author=Virginia+Woolf
- إليك قائمة بالكتب المكتوبة باللغة الإنجليزية:
$ curl http://localhost:8080/books?language=English
- حمِّل الصفحة الرابعة من الكتب:
$ curl http://localhost:8080/books?page=3
يمكننا أيضًا الجمع بين مَعلمات طلب البحث author وlanguage وbooks لتحسين البحث.
إنشاء واجهة REST API المستندة إلى الحاويات ونشرها
بما أنّنا سعداء بأنّ واجهة REST API تعمل وفقًا للخطة، هذا هو الوقت المناسب لنشرها على السحابة الإلكترونية، وتحديدًا على Cloud Run.
سننفّذ ذلك على خطوتَين:
- أولاً، أنشئ صورة الحاوية باستخدام Cloud Build من خلال تنفيذ الأمر التالي:
$ gcloud builds submit \
--tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
- بعد ذلك، يمكنك نشر الخدمة باستخدام الأمر الثاني التالي:
$ gcloud run deploy run-crud \
--image gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api \
--allow-unauthenticated \
--region=${REGION} \
--platform=managed
باستخدام الأمر الأول، تنشئ Cloud Build صورة الحاوية وتستضيفها في Container Registry. ينشر الأمر التالي صورة الحاوية من السجلّ، وينشرها في منطقة السحابة الإلكترونية.
يمكننا التحقّق مرة أخرى في واجهة مستخدم Cloud Console من أنّ خدمة Cloud Run تظهر الآن في القائمة:

آخر خطوة سننفّذها هنا هي استرداد عنوان URL لخدمة Cloud Run التي تم نشرها حديثًا، وذلك بفضل الأمر التالي:
$ export RUN_CRUD_SERVICE_URL=$(gcloud run services describe run-crud \
--region=${REGION} \
--platform=managed \
--format='value(status.url)')
سنحتاج إلى عنوان URL لواجهة Cloud Run REST API في القسم التالي، لأنّ رمز الواجهة الأمامية في App Engine سيتفاعل مع واجهة برمجة التطبيقات.
9. استضافة تطبيق ويب لتصفُّح المكتبة
آخر خطوة لإضافة بعض التألّق إلى هذا المشروع هي توفير واجهة أمامية على الويب تتفاعل مع واجهة REST API. لهذا الغرض، سنستخدم Google App Engine، مع بعض رموز JavaScript البرمجية من جهة العميل التي ستطلب البيانات من واجهة برمجة التطبيقات من خلال طلبات AJAX (باستخدام واجهة برمجة التطبيقات Fetch من جهة العميل).
على الرغم من أنّ تطبيقنا يتم نشره على وقت تشغيل Node.JS App Engine، إلا أنّه يتألف في الغالب من موارد ثابتة. لا يتضمّن التطبيق الكثير من رموز الخلفية، لأنّ معظم تفاعلات المستخدمين ستتم في المتصفّح من خلال JavaScript من جهة العميل. لن نستخدم أي إطار عمل JavaScript متطوّر للواجهة الأمامية، بل سنستخدم بعض JavaScript "العادي"، مع بعض "مكوّنات الويب" للواجهة باستخدام مكتبة مكوّنات الويب Shoelace:
- مربّع اختيار لتحديد لغة الكتاب:

- مكوّن بطاقة لعرض تفاصيل حول كتاب معيّن (بما في ذلك رمز شريطي لتمثيل رقم ISBN الخاص بالكتاب، باستخدام مكتبة JsBarcode):

- وزر لتحميل المزيد من الكتب من قاعدة البيانات:

عند دمج كل هذه المكوّنات المرئية معًا، ستظهر صفحة الويب الناتجة لتصفّح مكتبتنا على النحو التالي:

app.yaml ملف الإعداد
لنبدأ بالتعمّق في قاعدة الرموز البرمجية لتطبيق App Engine هذا من خلال إلقاء نظرة على ملف الإعدادات app.yaml. هذا ملف خاص بـ App Engine، ويسمح بإعداد عناصر مثل متغيّرات البيئة أو "المعالجات" المختلفة للتطبيق، أو تحديد أنّ بعض الموارد هي أصول ثابتة سيتم عرضها من خلال شبكة توصيل المحتوى (CDN) المضمّنة في App Engine.
runtime: nodejs14
env_variables:
RUN_CRUD_SERVICE_URL: CHANGE_ME
handlers:
- url: /js
static_dir: public/js
- url: /css
static_dir: public/css
- url: /img
static_dir: public/img
- url: /(.+\.html)
static_files: public/html/\1
upload: public/(.+\.html)
- url: /
static_files: public/html/index.html
upload: public/html/index\.html
- url: /.*
secure: always
script: auto
نحدّد أنّ تطبيقنا هو تطبيق Node.JS، وأنّنا نريد استخدام الإصدار 14.
بعد ذلك، نحدّد متغيّر بيئة يشير إلى عنوان URL الخاص بخدمة Cloud Run. علينا تعديل العنصر النائب CHANGE_ME باستخدام عنوان URL الصحيح (اطّلِع على كيفية تغيير ذلك أدناه).
بعد ذلك، نحدّد معالجات مختلفة. تشير الملفات الثلاثة الأولى إلى موقع رمز HTML وCSS وJavaScript من جهة العميل، ضمن المجلد public/ والمجلدات الفرعية. يشير الخيار الرابع إلى أنّ عنوان URL الجذر لتطبيق App Engine يجب أن يشير إلى الصفحة index.html. بهذه الطريقة، لن تظهر اللاحقة index.html في عنوان URL عند الوصول إلى جذر الموقع الإلكتروني. والأخير هو الإعداد التلقائي الذي سيوجه جميع عناوين URL الأخرى (/.*) إلى تطبيق Node.JS (أي الجزء "الديناميكي" من التطبيق، على عكس الأصول الثابتة التي وصفناها).
لنعدّل الآن عنوان URL لواجهة Web API لخدمة Cloud Run.
في دليل appengine-frontend/، شغِّل الأمر التالي لتعديل متغيّر البيئة الذي يشير إلى عنوان URL لواجهة REST API المستندة إلى Cloud Run:
$ sed -i -e "s|CHANGE_ME|${RUN_CRUD_SERVICE_URL}|" app.yaml
أو غيِّر السلسلة CHANGE_ME يدويًا في app.yaml باستخدام عنوان URL الصحيح:
env_variables:
RUN_CRUD_SERVICE_URL: CHANGE_ME
package.json ملف Node.JS
{
"name": "appengine-frontend",
"description": "Web frontend",
"license": "Apache-2.0",
"main": "index.js",
"engines": {
"node": "^14.0.0"
},
"dependencies": {
"express": "^4.17.1",
"isbn3": "^1.1.10"
},
"devDependencies": {
"nodemon": "^2.0.7"
},
"scripts": {
"start": "node index.js",
"dev": "nodemon --watch server --inspect index.js"
}
}
نؤكّد مرة أخرى أنّنا نريد تشغيل هذا التطبيق باستخدام Node.JS 14. نعتمد على إطار عمل Express، بالإضافة إلى وحدة isbn3 NPM للتحقّق من رموز ISBN الخاصة بالكتب.
في تبعيات التطوير، سنستخدم الوحدة nodemon لمراقبة تغييرات الملفات. على الرغم من أنّه يمكننا تشغيل تطبيقنا محليًا باستخدام npm start، وإجراء بعض التغييرات على الرمز، وإيقاف التطبيق باستخدام ^C، ثم إعادة تشغيله، إلا أنّ هذه العملية مملة بعض الشيء. بدلاً من ذلك، يمكننا استخدام الأمر التالي لإعادة تحميل التطبيق أو إعادة تشغيله تلقائيًا عند إجراء تغييرات:
$ npm run dev
index.js رمز Node.JS
const express = require('express');
const app = express();
app.use(express.static('public'));
const bodyParser = require('body-parser');
app.use(bodyParser.json());
نحتاج إلى إطار عمل الويب Express. نحدّد أنّ الدليل العام يحتوي على مواد عرض ثابتة يمكن عرضها (على الأقل عند التشغيل محليًا في وضع التطوير) بواسطة برنامج static الوسيط. أخيرًا، نحن بحاجة إلى body-parser لتحليل حمولات JSON.
لنلقِ نظرة على المسارين اللذين حدّدناهما:
app.get('/', async (req, res) => {
res.redirect('/html/index.html');
});
app.get('/webapi', async (req, res) => {
res.send(process.env.RUN_CRUD_SERVICE_URL);
});
سيتم إعادة توجيه أول نتيجة تطابق / إلى index.html في دليل public/html. بما أنّنا لا نعمل في وضع التطوير ضمن وقت تشغيل App Engine، لا يمكننا الاستفادة من ميزة توجيه عناوين URL في App Engine. لذلك، نعيد توجيه عنوان URL الجذر إلى ملف HTML.
ستعرض نقطة النهاية الثانية التي نحدّدها /webapi عنوان URL لواجهة برمجة تطبيقات REST في Cloud Run. بهذه الطريقة، سيعرف رمز JavaScript من جهة العميل المكان الذي يجب الاتصال به للحصول على قائمة الكتب.
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Book library web frontend: listening on port ${port}`);
console.log(`Node ${process.version}`);
console.log(`Web API endpoint ${process.env.RUN_CRUD_SERVICE_URL}`);
});
لإنهاء العملية، سنشغّل تطبيق الويب Express وسنستمع إلى المنفذ 8080 تلقائيًا.
index.html الصفحة
لن نراجع كل سطر من صفحة HTML الطويلة هذه. بدلاً من ذلك، لنستعرض بعض الأسطر الرئيسية.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/themes/base.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/shoelace.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/barcodes/JsBarcode.ean-upc.min.js"></script>
<script src="/js/app.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css">
يستورد السطران الأولان مكتبة مكونات الويب Shoelace (نص برمجي وورقة أنماط).
يستورد السطر التالي مكتبة JsBarcode لإنشاء الرموز الشريطية لرموز ISBN الخاصة بالكتب.
تستورد الأسطر الأخيرة رمز JavaScript الخاص بنا وورقة أنماط CSS، وكلاهما يقعان في الدليلَين الفرعيَين public/.
في body من صفحة HTML، نستخدم مكوّنات Shoelace مع علامات العناصر المخصّصة، مثل:
<sl-icon name="book-half"></sl-icon>
...
<sl-select id="language-select" placeholder="Select a language..." clearable>
<sl-menu-item value="English">English</sl-menu-item>
<sl-menu-item value="French">French</sl-menu-item>
...
</sl-select>
...
<sl-button id="more-button" type="primary" size="large">
More books...
</sl-button>
...
نستخدم أيضًا نماذج HTML وإمكانية ملء الخانات لتمثيل كتاب. سننشئ نُسخًا من هذا النموذج لتعبئة قائمة الكتب، وسنستبدل القيم في الخانات بتفاصيل الكتب:
<template id="book-card">
<sl-card class="card-overview">
...
<slot name="author">Author</slot>
...
</sl-card>
</template>
يكفي هذا القدر من HTML، فقد أوشكنا على الانتهاء من مراجعة الرمز. يتبقى جزء أخير مهم وهو رمز JavaScript app.js من جهة العميل الذي يتفاعل مع واجهة REST API.
رمز JavaScript من جهة العميل في app.js
نبدأ بمتتبِّع أحداث ذي مستوى أعلى ينتظر تحميل محتوى DOM:
document.addEventListener("DOMContentLoaded", async function(event) {
...
}
بعد أن يصبح جاهزًا، يمكننا إعداد بعض الثوابت والمتغيرات الرئيسية:
const serverUrlResponse = await fetch('/webapi');
const serverUrl = await serverUrlResponse.text();
console.log('Web API endpoint:', serverUrl);
const server = serverUrl + '/books';
var page = 0;
var language = '';
أولاً، سنسترد عنوان URL الخاص بواجهة REST API، وذلك بفضل رمز عقدة App Engine الذي يعرض متغير البيئة الذي ضبطناه في البداية في app.yaml. بفضل متغيّر البيئة، لم نضطر إلى تضمين عنوان URL لواجهة REST API في رمز الواجهة الأمامية، وذلك عند استدعاء نقطة النهاية /webapi من رمز JavaScript من جهة العميل.
نحدّد أيضًا المتغيّرين page وlanguage اللذين سنستخدمهما لتتبُّع تقسيم الصفحات وتصفية اللغة.
const moreButton = document.getElementById('more-button');
moreButton.addEventListener('sl-focus', event => {
console.log('Button clicked');
moreButton.blur();
appendMoreBooks(server, page++, language);
});
نضيف معالج أحداث إلى الزر لتحميل الكتب. عند النقر عليه، سيتم استدعاء الدالة appendMoreBooks().
const langSelect = document.getElementById('language-select');
langSelect.addEventListener('sl-change', event => {
page = 0;
language = event.srcElement.value;
document.getElementById('library').replaceChildren();
console.log(`Language selected: "${language}"`);
appendMoreBooks(server, page++, language);
});
وينطبق الأمر نفسه على مربّع الاختيار، حيث نضيف معالج أحداث ليتم إعلامنا بالتغييرات في اختيار اللغة. وكما هو الحال مع الزر، نستدعي أيضًا الدالة appendMoreBooks()، ونمرّر عنوان URL الخاص بواجهة REST API والصفحة الحالية واختيار اللغة.
لنلقِ نظرة على الدالة التي تسترد الكتب وتلحقها:
async function appendMoreBooks(server, page, language) {
const searchUrl = new URL(server);
if (!!page) searchUrl.searchParams.append('page', page);
if (!!language) searchUrl.searchParams.append('language', language);
const response = await fetch(searchUrl.href);
const books = await response.json();
...
}
في ما يلي، ننشئ عنوان URL الدقيق الذي سيتم استخدامه لطلب بيانات من واجهة REST API. هناك ثلاث مَعلمات طلب بحث يمكننا تحديدها عادةً، ولكن في واجهة المستخدم هذه، نحدّد مَعلمتَين فقط:
-
page: عدد صحيح يشير إلى الصفحة الحالية لتنظيم الكتب في صفحات -
language: سلسلة لغة للفلترة حسب اللغة المكتوبة
بعد ذلك، نستخدم Fetch API لاسترداد مصفوفة JSON التي تحتوي على تفاصيل الكتاب.
const linkHeader = response.headers.get('Link')
console.log('Link', linkHeader);
if (!!linkHeader && linkHeader.indexOf('rel="next"') > -1) {
console.log('Show more button');
document.getElementById('buttons').style.display = 'block';
} else {
console.log('Hide more button');
document.getElementById('buttons').style.display = 'none';
}
استنادًا إلى ما إذا كان العنوان Link متوفّرًا في الردّ، سنعرض الزر [More books...] أو نخفيه، لأنّ العنوان Link هو تلميح يخبرنا ما إذا كان هناك المزيد من الكتب التي لم يتم تحميلها بعد (سيكون هناك عنوان URL next في العنوان Link).
const library = document.getElementById('library');
const template = document.getElementById('book-card');
for (let book of books) {
const bookCard = template.content.cloneNode(true);
bookCard.querySelector('slot[name=title]').innerText = book.title;
bookCard.querySelector('slot[name=language]').innerText = book.language;
bookCard.querySelector('slot[name=author]').innerText = book.author;
bookCard.querySelector('slot[name=year]').innerText = book.year;
bookCard.querySelector('slot[name=pages]').innerText = book.pages;
const img = document.createElement('img');
img.setAttribute('id', book.isbn);
img.setAttribute('class', 'img-barcode-' + book.isbn)
bookCard.querySelector('slot[name=barcode]').appendChild(img);
library.appendChild(bookCard);
...
}
}
في القسم أعلاه من الدالة، لكل كتاب يتم عرضه من خلال واجهة REST API، سنستنسخ النموذج باستخدام بعض مكوّنات الويب التي تمثّل كتابًا، وسنملأ خانات النموذج بتفاصيل الكتاب.
JsBarcode('.img-barcode-' + book.isbn).EAN13(book.isbn, {fontSize: 18, textMargin: 0, height: 60}).render();
لجعل رمز ISBN أكثر جاذبية، نستخدم مكتبة JsBarcode لإنشاء رمز شريطي جميل مثل الرمز الموجود على الغلاف الخلفي للكتب الحقيقية.
تشغيل التطبيق واختباره على الجهاز المحلي
يكفي هذا القدر من الرموز البرمجية في الوقت الحالي، فقد حان الوقت لرؤية التطبيق أثناء العمل. سننفّذ ذلك أولاً على المستوى المحلي، ضِمن Cloud Shell، قبل النشر الفعلي.
نثبّت وحدات NPM التي يحتاجها تطبيقنا باستخدام الأمر التالي:
$ npm install
ونشغّل التطبيق إما بالطريقة المعتادة:
$ npm start
أو مع إعادة تحميل التغييرات تلقائيًا بفضل nodemon، باستخدام:
$ npm run dev
يتم تشغيل التطبيق على الجهاز، ويمكننا الوصول إليه من المتصفّح على العنوان http://localhost:8080.
نشر تطبيق App Engine
بعد التأكّد من أنّ تطبيقنا يعمل بشكل جيد على جهازك، حان الوقت لنشره على App Engine.
لتنفيذ التطبيق، لننفِّذ الأمر التالي:
$ gcloud app deploy -q
بعد دقيقة تقريبًا، من المفترض أن يتم نشر التطبيق.
سيكون التطبيق متاحًا على عنوان URL بالشكل التالي: https://${GOOGLE_CLOUD_PROJECT}.appspot.com.
استكشاف واجهة المستخدم لتطبيق الويب على App Engine
يمكنك الآن:
- انقر على الزر
[More books...]لتحميل المزيد من الكتب. - اختَر لغة معيّنة لعرض الكتب بهذه اللغة فقط.
- يمكنك إلغاء الاختيار باستخدام علامة التقاطع الصغيرة في مربّع الاختيار للرجوع إلى قائمة جميع الكتب.
10. التنظيف (اختياري)
إذا لم تكن تنوي الاحتفاظ بالتطبيق، يمكنك تنظيف الموارد لتوفير التكاليف ولتكون مواطنًا جيدًا للخدمات السحابية بشكل عام من خلال حذف المشروع بأكمله:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
11. تهانينا!
لقد أنشأنا مجموعة من الخدمات، بفضل Cloud Functions وApp Engine وCloud Run، لعرض نقاط نهاية مختلفة لواجهة Web API وواجهة أمامية على الويب، وذلك لتخزين مكتبة من الكتب وتعديلها وتصفّحها، مع اتّباع بعض أنماط التصميم الجيدة لتطوير واجهة REST API على طول الطريق.
المواضيع التي تناولناها
- وظائف السحابة الإلكترونية
- Cloud Firestore
- Cloud Run
- App Engine
مزيد من المعلومات
إذا أردت استكشاف هذا المثال الملموس وتوسيعه، إليك قائمة بالأشياء التي قد تحتاج إلى التحقيق فيها:
- يمكنك الاستفادة من API Gateway لتوفير واجهة برمجة تطبيقات مشتركة لوظيفة استيراد البيانات وحاوية REST API، أو لإضافة ميزات مثل التعامل مع مفاتيح واجهة برمجة التطبيقات للوصول إلى واجهة برمجة التطبيقات، أو تحديد حدود المعدّل لمستخدمي واجهة برمجة التطبيقات.
- نشر وحدة عقدة Swagger-UI في تطبيق App Engine لتوثيق واجهة REST API وتوفير ساحة اختبار لها
- في الواجهة الأمامية، بالإضافة إلى إمكانية التصفّح الحالية، أضِف شاشات إضافية لتعديل البيانات وإنشاء إدخالات كتب جديدة. بما أنّنا نستخدم قاعدة بيانات Cloud Firestore، يمكننا الاستفادة من ميزة الوقت الفعلي لتعديل بيانات الكتب المعروضة عند إجراء تغييرات.