1. بررسی اجمالی
هدف این کد لبه کسب تجربه با خدمات «بدون سرور» ارائه شده توسط Google Cloud Platform است:
- توابع ابری - برای استقرار واحدهای کوچک منطق تجاری به شکل توابع، که به رویدادهای مختلف واکنش نشان میدهند (پیامهای Pub/Sub، فایلهای جدید در فضای ذخیرهسازی ابری، درخواستهای HTTP و موارد دیگر)،
- App Engine - برای استقرار و ارائه برنامههای وب، APIهای وب، پشتیبانهای تلفن همراه، داراییهای ثابت، با قابلیتهای افزایش و کاهش سریع مقیاس،
- Cloud Run - برای استقرار و مقیاس بندی کانتینرهایی که می توانند شامل هر زبان، زمان اجرا یا کتابخانه ای باشند.
و کشف چگونگی بهرهگیری از این خدمات بدون سرور برای استقرار و مقیاسبندی Web و REST APIها، در حالی که برخی از اصول طراحی RESTful خوب را در طول مسیر مشاهده میکنید.
در این کارگاه، ما یک کاوشگر قفسه کتاب ایجاد خواهیم کرد که شامل موارد زیر است:
- یک تابع Cloud: برای وارد کردن مجموعه داده اولیه کتابهای موجود در کتابخانه ما، در پایگاه داده اسناد Cloud Firestore ،
- یک محفظه Cloud Run: که یک REST API را بر روی محتوای پایگاه داده ما نشان می دهد،
- یک صفحه وب موتور App: برای مرور لیست کتابها، با تماس با REST API ما.
در اینجا ظاهر وب در انتهای این کد لبه به چه صورت خواهد بود:
چیزی که یاد خواهید گرفت
- توابع ابری
- Cloud Firestore
- Cloud Run
- موتور برنامه
2. راه اندازی و الزامات
تنظیم محیط خود به خود
- به Google Cloud Console وارد شوید و یک پروژه جدید ایجاد کنید یا از یک موجود استفاده مجدد کنید. اگر قبلاً یک حساب Gmail یا Google Workspace ندارید، باید یک حساب ایجاد کنید .
- نام پروژه نام نمایشی برای شرکت کنندگان این پروژه است. این یک رشته کاراکتری است که توسط API های Google استفاده نمی شود. همیشه می توانید آن را به روز کنید.
- شناسه پروژه در تمام پروژههای Google Cloud منحصربهفرد است و تغییرناپذیر است (پس از تنظیم نمیتوان آن را تغییر داد). Cloud Console به طور خودکار یک رشته منحصر به فرد تولید می کند. معمولاً برای شما مهم نیست که چیست. در اکثر کدها، باید شناسه پروژه خود را ارجاع دهید (معمولاً با نام
PROJECT_ID
شناخته می شود). اگر شناسه تولید شده را دوست ندارید، ممکن است یک شناسه تصادفی دیگر ایجاد کنید. از طرف دیگر، میتوانید خودتان را امتحان کنید، و ببینید آیا در دسترس است یا خیر. پس از این مرحله نمی توان آن را تغییر داد و در طول مدت پروژه باقی می ماند. - برای اطلاع شما، یک مقدار سوم وجود دارد، یک شماره پروژه ، که برخی از API ها از آن استفاده می کنند. در مورد هر سه این مقادیر در مستندات بیشتر بیاموزید.
- در مرحله بعد، برای استفاده از منابع Cloud/APIها باید صورتحساب را در کنسول Cloud فعال کنید . اجرا کردن از طریق این کد لبه هزینه زیادی نخواهد داشت. برای خاموش کردن منابع برای جلوگیری از تحمیل صورتحساب فراتر از این آموزش، میتوانید منابعی را که ایجاد کردهاید حذف کنید یا پروژه را حذف کنید. کاربران جدید Google Cloud واجد شرایط برنامه آزمایشی رایگان 300 دلاری هستند.
Cloud Shell را راه اندازی کنید
در حالی که Google Cloud را می توان از راه دور از لپ تاپ شما کار کرد، در این کد لبه از Google Cloud Shell استفاده خواهید کرد، یک محیط خط فرمان که در Cloud اجرا می شود.
از Google Cloud Console ، روی نماد Cloud Shell در نوار ابزار بالا سمت راست کلیک کنید:
تهیه و اتصال به محیط فقط چند لحظه طول می کشد. وقتی تمام شد، باید چیزی شبیه به این را ببینید:
این ماشین مجازی با تمام ابزارهای توسعه که شما نیاز دارید بارگذاری شده است. این یک فهرست اصلی 5 گیگابایتی دائمی را ارائه می دهد و در Google Cloud اجرا می شود و عملکرد و احراز هویت شبکه را تا حد زیادی افزایش می دهد. تمام کارهای شما در این کد لبه را می توان در یک مرورگر انجام داد. شما نیازی به نصب چیزی ندارید.
3. محیط را آماده کنید و APIهای ابری را فعال کنید
برای استفاده از خدمات مختلفی که در طول این پروژه نیاز داریم، چند API را فعال می کنیم. ما این کار را با اجرای دستور زیر در 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
این 3 فهرست مربوط به جستجوهایی است که ما بر اساس نویسنده یا زبان انجام خواهیم داد، در حالی که سفارش را در مجموعه از طریق یک فیلد به روز نگه می داریم.
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
- این کانتینر یک Web API را برای دسترسی به داده های کتاب ذخیره شده در Cloud Firestore در معرض دید قرار می دهد. -
appengine-frontend
- این برنامه وب موتور App یک صفحه ظاهری ساده فقط خواندنی را برای مرور لیست کتابها نمایش می دهد.
5. نمونه داده های کتابخانه کتاب
در پوشه داده، ما یک فایل books.json
داریم که حاوی لیستی از صد کتاب است که احتمالاً ارزش خواندن را دارد. این سند JSON یک آرایه حاوی اشیاء JSON است. بیایید به شکل دادههایی که از طریق یک تابع ابری دریافت میکنیم نگاهی بیندازیم:
[
{
"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 را نیز فراهم می کند، بنابراین نیازی نیست که آن را به عنوان یک وابستگی اعلام کنیم.
در وابستگیهای توسعه، ما Functions Framework ( @google-cloud/functions-framework
) را اعلام میکنیم که چارچوب زمان اجرا مورد استفاده برای فراخوانی توابع شما است. این یک چارچوب منبع باز است که می توانید به صورت محلی روی دستگاه خود (در مورد ما، در داخل Cloud Shell) برای اجرای توابع بدون استفاده از هر بار تغییر استفاده کنید، بنابراین حلقه بازخورد توسعه را بهبود می بخشید.
برای نصب وابستگی ها از دستور install
استفاده کنید:
$ npm install
اسکریپت start
از Functions Framework استفاده می کند تا دستوری را به شما بدهد که می توانید از آن برای اجرای محلی تابع با دستور زیر استفاده کنید:
$ npm start
میتوانید از curl یا پیشنمایش وب پوسته ابری برای درخواستهای 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
صادر می کنیم. این تابعی است که بعداً در هنگام استقرار آن اعلام خواهیم کرد.
دو دستورالعمل بعدی بررسی می کنند که:
- ما فقط درخواستهای 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
برای اجرای تابع به صورت محلی، به لطف Functions Framework، از دستور start
script که در 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"}
همچنین میتوانید برای بررسی اینکه آیا دادهها واقعاً در Firestore ذخیره شدهاند، به رابط کاربری Cloud Console بروید:
در اسکرین شات بالا، مجموعه books
ایجاد شده، لیست اسناد کتاب که با کد شابک کتاب مشخص شده اند و جزئیات مربوط به آن مدخل کتاب خاص را در سمت راست می بینیم.
استقرار تابع در ابر
برای استقرار تابع در توابع 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
(عملکرد جاوا اسکریپت صادر شده) به عنوان نقطه ورودی استفاده می کنیم.
پس از چند دقیقه یا کمتر، عملکرد در فضای ابری مستقر می شود. در رابط کاربری Cloud Console، باید تابع ظاهر شود:
در خروجی استقرار، باید بتوانید URL تابع خود را ببینید که از یک قرارداد نامگذاری خاصی پیروی می کند ( https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}
) و البته، شما همچنین می توانید این نشانی اینترنتی راه انداز HTTP را در رابط کاربری Cloud Console، در تب trigger پیدا کنید:
همچنین می توانید 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"}
اکنون که تابع import ما مستقر شده و آماده است، و دادههای نمونه خود را آپلود کردهایم، وقت آن است که REST API را توسعه دهیم که این مجموعه داده را نشان میدهد.
7. قرارداد REST API
اگرچه ما قرارداد API را با استفاده از مشخصات Open API تعریف نمی کنیم، اما قصد داریم به نقاط پایانی مختلف REST API خود نگاهی بیندازیم.
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
}
دریافت /کتاب
فهرستی از همه کتابها را دریافت کنید، بهطور بالقوه بر اساس نویسنده و/یا زبان فیلتر شده و هر بار با پنجرههای 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 API را در یک ظرف مستقر کرده و در معرض دید قرار دهید
کد را کاوش کنید
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
بود.
برنامه API وب ما به موارد زیر بستگی دارد:
- ماژول Firestore NPM برای دسترسی به داده های کتاب در پایگاه داده،
- کتابخانه
cors
برای رسیدگی به درخواستهای CORS (Cross Origin Resource Sharing)، زیرا REST API ما از کد کلاینت برنامه کاربردی وب App Engine ما فراخوانی میشود. - چارچوب Express، که چارچوب وب ما برای طراحی API ما خواهد بود،
- و سپس ماژول
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 که با API خود رد و بدل می شوند استفاده می کنیم.
ماژول 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 را تجزیه می کند و در صورت نامعتبر بودن کدهای ISBN با کد وضعیت 406
در پاسخ پاسخ می دهد.
-
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
فیلتر کردیم، اما در این بخش، فهرست کتابها را بر اساس آخرین تاریخ بهروزرسانی مرتب میکنیم (آخرین بهروزرسانی اول است). و همچنین با تعیین یک محدودیت (تعداد عناصر برای بازگشت) و یک افست (نقطه شروعی که از آن دسته بعدی کتاب ها را برگردانیم) نتیجه را صفحه بندی می کنیم.
ما پرس و جو را اجرا می کنیم، عکس فوری داده ها را دریافت می کنیم و آن نتایج را در یک آرایه جاوا اسکریپت قرار می دهیم که در انتهای تابع برگردانده می شود.
بیایید توضیحات این نقطه پایانی را با نگاهی به یک تمرین خوب به پایان برسانیم: استفاده از هدر 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
andPOST /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
) برگردید. ما فیلدهای کتاب را از محموله ارسال شده در متن درخواست بازیابی می کنیم. سپس جزئیات کتاب را در Firestor ذخیره می کنیم. بازگشت 201
در موفقیت و 400
در شکست.
هنگام بازگشت موفقیت آمیز، هدر مکان را نیز تنظیم می کنیم تا به مشتری API که منبع تازه ایجاد شده در آن قرار دارد، نشانه هایی ارائه دهیم. هدر به صورت زیر خواهد بود:
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}`});
}
});
مثل همیشه، ما بررسی می کنیم که آیا شابک معتبر است یا خیر. برای بازیابی کتاب از Firestor درخواست می کنیم. ویژگی 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 build -t crud-web-api . $ docker run --rm -p 8080:8080 -it crud-web-api
اجرای درون داکر نیز یک راه عالی برای بررسی مضاعف این است که کانتینریسازی برنامه ما به خوبی اجرا میشود، زیرا آن را در فضای ابری با Cloud Build میسازیم.
تست API
صرف نظر از نحوه اجرای کد 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
- بازیابی کتاب توسط شابک:
$ 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، در 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 ما با API تعامل خواهد داشت.
9. یک برنامه وب را برای مرور کتابخانه میزبانی کنید
آخرین قطعه از پازل برای افزودن کمی زرق و برق به این پروژه، ارائه یک صفحه وب است که با REST API ما تعامل داشته باشد. برای این منظور، ما از Google App Engine، همراه با کد جاوا اسکریپت مشتری استفاده خواهیم کرد که API را از طریق درخواست های AJAX (با استفاده از Fetch API سمت کلاینت) فراخوانی می کند.
برنامه ما، اگرچه در زمان اجرا Node.JS App Engine مستقر شده است، اما بیشتر از منابع ثابت ساخته شده است! کد پشتیبان زیادی وجود ندارد، زیرا بیشتر تعامل کاربر در مرورگر از طریق جاوا اسکریپت سمت کلاینت خواهد بود. ما از هیچ چارچوب جاوا اسکریپت ظاهری فانتزی استفاده نخواهیم کرد، فقط از مقداری جاوا اسکریپت «وانیلی» با چند مؤلفه وب برای رابط کاربری با استفاده از کتابخانه مؤلفه وب Shoelace استفاده خواهیم کرد:
- یک کادر انتخاب برای انتخاب زبان کتاب:
- یک جزء کارت برای نمایش جزئیات مربوط به یک کتاب خاص (از جمله یک بارکد برای نشان دادن ISBN کتاب، با استفاده از کتابخانه JsBarcode ):
- و یک دکمه برای بارگیری کتاب های بیشتر از پایگاه داده:
هنگامی که تمام آن اجزای بصری را با هم ترکیب می کنیم، صفحه وب به دست آمده برای مرور کتابخانه ما به صورت زیر خواهد بود:
فایل پیکربندی app.yaml
بیایید با مشاهده فایل پیکربندی app.yaml
آن، غواصی را در پایه کد این برنامه App Engine شروع کنیم. این فایلی است که مخصوص App Engine است و به پیکربندی مواردی مانند متغیرهای محیطی، «هندلرهای» مختلف برنامه، یا تعیین اینکه برخی منابع داراییهای ثابت هستند، اجازه میدهد تا توسط موتور داخلی App Engine ارائه شوند CDN.
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 را با نشانی اینترنتی صحیح به روز کنیم (در مورد نحوه تغییر آن به زیر مراجعه کنید).
پس از آن، ما هندلرهای مختلفی را تعریف می کنیم. 3 مورد اول به محل کد سمت کلاینت HTML، CSS و جاوا اسکریپت، زیر پوشه public/
و زیر پوشه های آن اشاره می کنند. مورد چهارم نشان می دهد که URL اصلی برنامه App Engine ما باید در صفحه index.html
باشد. به این ترتیب، در هنگام دسترسی به ریشه وب سایت، پسوند index.html
را در URL مشاهده نخواهیم کرد. و آخرین مورد پیشفرض است که همه URLهای دیگر ( /.*
) را به برنامه Node.JS ما هدایت میکند (یعنی بخش "دینامیک" برنامه، برخلاف داراییهای ثابتی که توضیح دادیم).
بیایید اکنون URL API Web سرویس Cloud Run را به روز کنیم.
در دایرکتوری appengine-frontend/
، دستور زیر را برای بهروزرسانی متغیر محیطی که به URL API REST مبتنی بر 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
فایل 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 اجرا کنیم. برای اعتبارسنجی کدهای ISBN کتاب ها به چارچوب Express و همچنین ماژول isbn3
NPM وابسته هستیم.
در وابستگی های توسعه، ما از ماژول 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 Cloud RUN REST API ما را برمی گرداند. به این ترتیب، کد جاوا اسکریپت سمت کلاینت میداند که برای دریافت لیست کتابها با کجا تماس بگیرد.
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 کتاب را ایجاد کند.
آخرین خطوط کد جاوا اسکریپت و شیوه نامه 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 کافی است، ما تقریباً با بررسی کد به پایان رسیده ایم. آخرین بخش گوشتی باقی مانده: کد جاوا اسکریپت سمت کلاینت app.js
که با REST API ما تعامل دارد.
کد جاوا اسکریپت سمت کلاینت 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 = '';
ابتدا، به لطف کد گره App Engine ما که متغیر محیطی را که ابتدا در app.yaml
تنظیم کرده بودیم، نشانی وب REST API خود را واکشی می کنیم. به لطف متغیر محیطی، نقطه پایانی /webapi
، که از کد سمت کلاینت جاوا اسکریپت فراخوانی میشود، مجبور نبودیم URL API 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 API 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 API استفاده کنیم. سه پارامتر پرس و جو وجود دارد که می توانیم به طور معمول آنها را مشخص کنیم، اما در اینجا در این UI، ما فقط دو پارامتر را مشخص می کنیم:
-
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
راهنمایی است که به ما میگوید آیا هنوز کتابهای بیشتری برای بارگیری وجود دارد یا نه (یک next
وجود خواهد داشت). URL در هدر 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
پس از حدود یک دقیقه، برنامه باید مستقر شود.
این برنامه در یک نشانی اینترنتی به این شکل در دسترس خواهد بود: https://${GOOGLE_CLOUD_PROJECT}.appspot.com
.
کاوش رابط کاربری برنامه وب موتور App ما
اکنون می توانید:
- برای بارگیری کتابهای بیشتر، روی دکمه
[More books...]
کلیک کنید. - یک زبان خاص را انتخاب کنید تا کتابها را فقط به آن زبان ببینید.
- میتوانید انتخاب را با علامت ضربدر کوچک در کادر انتخاب پاک کنید تا به فهرست همه کتابها بازگردید.
10. تمیز کردن (اختیاری)
اگر قصد ندارید برنامه را نگه دارید، می توانید با حذف کل پروژه، منابع را پاکسازی کنید تا در هزینه ها صرفه جویی کنید و در کل شهروند ابری خوبی باشید:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
11. تبریک!
ما به لطف توابع Cloud، App Engine و Cloud Run مجموعهای از خدمات را ایجاد کردیم تا نقاط پایانی Web API و ظاهر وب را در معرض دید قرار دهیم، برای ذخیره، بهروزرسانی و مرور کتابخانهای از کتابها، از الگوهای طراحی خوب برای توسعه REST API پیروی کنیم. راه
آنچه را پوشش داده ایم
- توابع ابری
- Cloud Firestore
- Cloud Run
- موتور برنامه
جلوتر رفتن
اگر می خواهید این مثال عینی را بیشتر بررسی کنید و آن را گسترش دهید، در اینجا لیستی از مواردی وجود دارد که ممکن است بخواهید بررسی کنید:
- از API Gateway برای ارائه یک نمای مشترک API برای تابع وارد کردن داده و REST API ظرف، برای افزودن ویژگیهایی مانند کنترل کلیدهای API برای دسترسی به API یا تعریف محدودیتهای نرخ برای مصرفکنندگان API استفاده کنید.
- ماژول گره Swagger-UI را در برنامه App Engine مستقر کنید تا یک زمین بازی آزمایشی را برای API REST مستند و ارائه دهید.
- در قسمت جلوی ، فراتر از قابلیت مرور موجود ، برای ویرایش داده ها ، صفحه های اضافی اضافه کنید ، ورودی های جدید کتاب ایجاد کنید. همچنین ، از آنجا که ما از پایگاه داده Cloud Firestore استفاده می کنیم ، از ویژگی زمان واقعی خود برای به روزرسانی داده های کتاب نمایش داده شده در هنگام ایجاد تغییرات استفاده می کنیم.