1. نظرة عامة
والهدف من هذا الدرس التطبيقي حول الترميز هو اكتساب الخبرة في استخدام "بدون خادم" الخدمات التي تقدمها Google Cloud Platform:
- دوال Cloud: لنشر وحدات صغيرة من منطق النشاط التجاري على شكل وظائف، والتي تستجيب لأحداث مختلفة (رسائل النشر/الاشتراك، والملفات الجديدة في Cloud Storage، وطلبات HTTP، والمزيد)،
- App Engine - لنشر تطبيقات الويب وواجهات برمجة تطبيقات الويب والخلفيات للأجهزة الجوّالة ومواد العرض الثابتة وعرضها، مع إمكانات توسيع وخفض بسرعة،
- التشغيل في السحابة الإلكترونية: لنشر الحاويات وتغيير حجمها، التي يمكن أن تحتوي على أي لغة أو بيئة تشغيل أو مكتبة.
واكتشاف كيفية الاستفادة من هذه الخدمات بدون خادم لنشر واجهات برمجة تطبيقات Web وREST وتوسيع نطاقها، مع الاطّلاع أيضًا على بعض مبادئ تصميم REST الملائمة طوال الوقت.
في ورشة العمل هذه، سننشئ مستكشف رف كتب يتكون مما يلي:
- دالة Cloud Function: لاستيراد مجموعة البيانات الأولية من الكتب المتوفرة في مكتبتنا في قاعدة بيانات مستندات Cloud Firestore،
- حاوية Cloud Run: تعرض واجهة REST API على محتوى قاعدة البيانات
- واجهة ويب App Engine الأمامية: لتصفح قائمة الكتب، من خلال طلب واجهة برمجة تطبيقات REST.
إليك ما ستبدو عليه الواجهة الأمامية للويب في نهاية هذا الدرس التطبيقي حول الترميز:
المعلومات التي ستطّلع عليها
- وظائف السحابة الإلكترونية
- 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 APIs
من أجل استخدام الخدمات المختلفة التي سنحتاجها خلال هذا المشروع، سنقوم بتمكين بعض واجهات برمجة التطبيقات. وسننفّذ ذلك من خلال إطلاق الأمر التالي في 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. الحصول على الرمز
احصل على الرمز من مستودع جيت هب التالي:
$ 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
— ستعرض هذه الحاوية واجهة برمجة تطبيقات Web API للوصول إلى بيانات الكتب المخزَّنة في Cloud Firestore.appengine-frontend
— سيعرض تطبيق الويب App Engine هذا واجهة أمامية بسيطة للقراءة فقط لتصفح قائمة الكتب.
5- نموذج بيانات مكتبة الكتب
في مجلد البيانات، لدينا ملف books.json
يحتوي على قائمة بمئات الكتب، والتي قد تستحق القراءة. مستند JSON هذا هو مصفوفة تحتوي على كائنات JSON. لنلقِ نظرة على شكل البيانات التي سننقلها عبر دالة Cloud:
[
{
"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- نقطة نهاية دالة لاستيراد نموذج بيانات الكتاب
في هذا القسم الأول، سننفذ نقطة النهاية التي سيتم استخدامها لاستيراد نموذج بيانات الكتاب. سنستخدم دوال السحابة لهذا الغرض.
استكشاف الرمز
لنبدأ بإلقاء نظرة على ملف 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 على الويب، لذلك لا نحتاج إلى التعريف عنه كتبعية.
في قسم الموارد التابعة للتطوير، نُعلن عن إطار عمل الدوال (@google-cloud/functions-framework
)، وهو إطار عمل وقت التشغيل المُستخدَم لتنفيذ الوظائف. وهو عبارة عن إطار عمل مفتوح المصدر يمكنك استخدامه أيضًا محليًا على جهازك (في هذه الحالة، داخل Cloud Shell) لتنفيذ الوظائف بدون نشرها في كل مرة تُجري فيها تغييرًا، وبالتالي تحسين حلقة ملاحظات التطوير.
لتثبيت الاعتماديات، استخدِم الأمر install
:
$ npm install
يستخدم النص البرمجي start
إطار عمل الدوال لإعطاء أمر يمكنك استخدامه لتشغيل الدالة على الجهاز باستخدام التعليمات التالية:
$ 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;
}
...
})
نحن بصدد تصدير دالة JavaScript parseBooks
. وهذه هي الدالة التي سنعلن عنها عند تفعيلها لاحقًا.
تتحقق التعليمات التالية من ما يلي:
- نحن نقبل فقط طلبات 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
للإشارة إلى تعذّر إتمام العملية. في الحالات الأخرى، يمكننا عرض الردّ "حسنًا" مع رمز الحالة 202
الذي يشير إلى قبول طلب الحفظ المجمّع.
تشغيل دالة الاستيراد واختبارها
قبل تشغيل التعليمة البرمجية، سنقوم بتثبيت التبعيات مع:
$ npm install
لتشغيل الدالة محليًا، بفضل إطار عمل الدوال، سنستخدم أمر النص البرمجي start
الذي حدّدناه في package.json
:
$ 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، سنستخدم الأمر التالي في دليل 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 التي تعرض مجموعة البيانات هذه.
7. عقد REST API
على الرغم من أننا لا نحدد عقد واجهة برمجة التطبيقات باستخدام، على سبيل المثال، مواصفات واجهة برمجة التطبيقات المفتوحة، إلا أننا سنلقي نظرة على نقاط النهاية المختلفة لواجهة برمجة تطبيقات REST.
تتبادل واجهات برمجة التطبيقات كائنات 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
}
الحصول على /books
يمكنك الحصول على قائمة بجميع الكتب، ومن المحتمل فلترتها حسب المؤلف و/أو اللغة، وتقسيمها إلى صفحات حسب نوافذ تتضمن 10 نتائج في المرة الواحدة.
حمولة الجسم: لا شيء.
معلمات طلب البحث:
author
(اختياري): يتيح هذا الخيار فلترة قائمة الكتب حسب المؤلف.language
(اختياري): يتيح لك هذا الخيار فلترة قائمة الكتب حسب اللغة.page
(اختيارية، القيمة التلقائية = 0) — تشير إلى ترتيب صفحة النتائج المطلوب عرضها.
يعرض: مصفوفة JSON لكائنات الكتب.
رموز الحالة:
200
— عندما ينجح الطلب في جلب قائمة الكتب،400
: في حال حدوث خطأ.
POST /books وPOST /books/{isbn}
انشر حمولة كتب جديدة إما باستخدام مَعلمة مسار isbn
(في هذه الحالة، لا يكون الرمز isbn
مطلوبًا في حمولة الكتاب) أو بدون (في هذه الحالة، يجب أن يتوفّر الرمز isbn
في حمولة الكتاب).
حمولة المحتوى: كائن كتاب
معلمات طلب البحث: بلا.
يعرض: لا شيء.
رموز الحالة:
201
: عند تخزين الكتاب بنجاح،406
: إذا كان رمزisbn
غير صالح400
: في حال حدوث خطأ.
الحصول على /books/{isbn}
لاسترداد كتاب من المكتبة، الذي يتم تحديده من خلال رمز isbn
، والذي يتم تمريره كمَعلمة مسار
حمولة الجسم: لا شيء.
معلمات طلب البحث: بلا.
يعرض: كائن JSON للكتاب أو كائن خطأ إذا لم يكن الكتاب موجودًا.
رموز الحالة:
200
: إذا تم العثور على الكتاب في قاعدة البيانات،400
: في حال حدوث خطأ،404
: في حال تعذُّر العثور على الكتاب،406
: إذا كان رمزisbn
غير صالح
إدراج /books/{isbn}
لتعديل كتاب حالي، يتم تحديده من خلال تمرير isbn
كمَعلمة مسار.
حمولة المحتوى: كائن كتاب لا يمكن تمرير سوى الحقول التي تحتاج إلى تحديث، أما الحقول الأخرى فتكون اختيارية.
معلمات طلب البحث: بلا.
يعرض: الكتاب الذي تم تحديثه.
رموز الحالة:
200
: عند تعديل الكتاب بنجاح،400
: في حال حدوث خطأ،406
: إذا كان رمزisbn
غير صالح
حذف /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" ]
نستخدم صورة "slim" من 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
.
يعتمد تطبيق واجهة برمجة تطبيقات الويب على ما يلي:
- وحدة Firestore NPM للوصول إلى بيانات الكتاب في قاعدة البيانات،
- مكتبة
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. نستخدم وحدة 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;
}
سنستخدم وحدة NPM isbn3
لتحليل رموز 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
. نسترد بيانات الكتاب وننشئ كائن 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 / خادم العقدة
أخيرًا وليس آخرًا، يتم تلقائيًا بدء تشغيل الخادم، عبر المنفذ 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 (مباشرةً عبر العقدة أو من خلال صورة حاوية 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 الحاوية
من دواعي سرورنا أن واجهة برمجة تطبيقات REST تعمل وفقًا للخطة، وأن الوقت مناسب لنشرها في السحابة الإلكترونية على 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 في القسم التالي، حيث سيتفاعل رمز واجهة App Engine الأمامية مع واجهة برمجة التطبيقات.
9. استضافة تطبيق ويب لتصفّح المكتبة
يتمثل الجزء الأخير من اللغز لإضافة بعض اللمعان إلى هذا المشروع في توفير واجهة أمامية على الويب تتفاعل مع واجهة برمجة تطبيقات REST. ولهذا الغرض، سنستخدم Google App Engine مع رمز JavaScript للعميل الذي يستدعي واجهة برمجة التطبيقات من خلال طلبات AJAX (باستخدام واجهة برمجة التطبيقات Fetch من جهة العميل).
على الرغم من نشر تطبيقنا في وقت تشغيل Node.JS App Engine، فهو مصنوع في الغالب من موارد ثابتة! لا يتوفّر الكثير من رموز الخلفية، إذ إنّ معظم تفاعل المستخدم سيكون من خلال المتصفّح من خلال JavaScript من جهة العميل. لن نستخدم أي إطار عمل JavaScript رائع للواجهة الأمامية، بل سنستخدم فقط بعض لغة JavaScript "vanilla"، مع القليل من مكونات الويب لواجهة المستخدم باستخدام مكتبة مكونات الويب Shoewear:
- مربع اختيار لاختيار لغة الكتاب:
- عنصر بطاقة لعرض تفاصيل حول كتاب معين (بما في ذلك الرمز الشريطي لتمثيل رقم 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 لخدمة تشغيل السحابة. سنحتاج إلى تحديث العنصر النائب CHANGE_ME باستخدام عنوان URL الصحيح (انظر أدناه لمعرفة كيفية تغيير ذلك).
بعد ذلك، نعرّف معالِجات مختلفة. تشير العناصر الثلاثة الأولى إلى موقع رمز HTML وCSS وJavaScript من جهة العميل ضمن مجلد public/
ومجلداته الفرعية. بينما يشير العنوان الرابع إلى أنّ عنوان URL الجذر لتطبيق App Engine يجب أن يشير إلى صفحة index.html
. بهذه الطريقة، لن تظهر اللاحقة index.html
في عنوان URL عند الوصول إلى جذر الموقع الإلكتروني. أما الأخير هو العنوان التلقائي الذي سيوجِّه جميع عناوين URL الأخرى (/.*
) إلى تطبيق Node.JS (أي الجزء "الديناميكي" في التطبيق، على عكس مواد العرض الثابتة التي وصفناها).
دعونا الآن نحدّث عنوان URL لواجهة برمجة تطبيقات الويب الخاصة بخدمة تشغيل السحابة الإلكترونية.
في دليل appengine-frontend/
، شغِّل الأمر التالي لتعديل متغيّر البيئة الذي يشير إلى عنوان URL لواجهة برمجة تطبيقات REST المستندة إلى Cloud Run-based:
$ 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
ملف Node.JS package.json
{
"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
رمز Node.JS في index.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 لواجهة برمجة تطبيقات Cloud RUN REST. بهذه الطريقة، سيعرف رمز 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">
يستورد أول سطرين مكتبة مكونات الويب Shoeسمة (نص برمجي وورقة أنماط).
يستورد السطر التالي مكتبة JsBarcode، لإنشاء الرموز الشريطية لرموز أرقام ISBN للكتاب.
تستورد الأسطر الأخيرة رمز JavaScript وورقة أنماط CSS، والموجودة في أدلة public/
الفرعية.
في body
من صفحة HTML، نستخدم مكونات "رباط الحذاء" مع علامات العناصر المخصصة الخاصة بها، مثل:
<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.
رمز 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، وذلك بفضل رمز عقدة App Engine الذي يعرض متغير البيئة الذي تم ضبطه بشكل مبدئي في app.yaml
. بفضل متغيّر البيئة، نقطة النهاية /webapi
، التي تم استدعاؤها من رمز JavaScript من جهة العميل، لم نضطر إلى إجراء ترميز ثابت لعنوان URL لواجهة برمجة التطبيقات REST في رمز الواجهة الأمامية.
نعرّف أيضًا المتغيّرات 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 والصفحة الحالية واختيار اللغة.
إذًا، لنلقِ نظرة على الدالة التي تجلب الكتب وتُلحقها:
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. هناك ثلاث مَعلمات طلب بحث يمكننا تحديدها عادةً، ولكن في واجهة المستخدم هذه، نحدِّد فقط مَعلمتَين:
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، سنستنسخ النموذج مع بعض مكونات الويب التي تمثل كتابًا، ونقوم بتعبئة خانات النموذج بتفاصيل الكتاب.
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 على طول المسار.
النقاط التي تناولناها
- وظائف السحابة الإلكترونية
- Cloud Firestore
- Cloud Run
- App Engine
تطوير قناتك
إذا كنت ترغب في استكشاف هذا المثال الملموس وتوسيعه، فإليك قائمة بالأشياء التي قد ترغب في التحقق منها:
- يمكنك الاستفادة من بوابة واجهة برمجة التطبيقات لتوفير واجهة شائعة لواجهة برمجة التطبيقات لوظيفة استيراد البيانات وحاوية واجهة برمجة تطبيقات REST، وذلك لإضافة ميزات مثل التعامل مع مفاتيح واجهة برمجة التطبيقات للوصول إلى واجهة برمجة التطبيقات، أو تحديد قيود المعدل لمستهلكي واجهات برمجة التطبيقات.
- انشر وحدة العقدة Swagger-UI في تطبيق App Engine لتوثيق وتقديم مساحة اختبار لـ REST API.
- في الواجهة الأمامية، بخلاف إمكانية التصفح الحالية، أضف شاشات إضافية لتعديل البيانات وإنشاء إدخالات جديدة للكتب. بالإضافة إلى ذلك، وبما أنّنا نستخدم قاعدة بيانات Cloud Firestore، يمكنك الاستفادة من ميزة الوقت الفعلي لتعديل بيانات الكتاب المعروضة عند إجراء التغييرات.