کارگاه APIهای وب بدون سرور

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 ما.

در اینجا ظاهر وب در انتهای این کد لبه به چه صورت خواهد بود:

705e014da0ca5e90.png

چیزی که یاد خواهید گرفت

  • توابع ابری
  • Cloud Firestore
  • Cloud Run
  • موتور برنامه

2. راه اندازی و الزامات

تنظیم محیط خود به خود

  1. به Google Cloud Console وارد شوید و یک پروژه جدید ایجاد کنید یا از یک موجود استفاده مجدد کنید. اگر قبلاً یک حساب Gmail یا Google Workspace ندارید، باید یک حساب ایجاد کنید .

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • نام پروژه نام نمایشی برای شرکت کنندگان این پروژه است. این یک رشته کاراکتری است که توسط API های Google استفاده نمی شود. همیشه می توانید آن را به روز کنید.
  • شناسه پروژه در تمام پروژه‌های Google Cloud منحصربه‌فرد است و تغییرناپذیر است (پس از تنظیم نمی‌توان آن را تغییر داد). Cloud Console به طور خودکار یک رشته منحصر به فرد تولید می کند. معمولاً برای شما مهم نیست که چیست. در اکثر کدها، باید شناسه پروژه خود را ارجاع دهید (معمولاً با نام PROJECT_ID شناخته می شود). اگر شناسه تولید شده را دوست ندارید، ممکن است یک شناسه تصادفی دیگر ایجاد کنید. از طرف دیگر، می‌توانید خودتان را امتحان کنید، و ببینید آیا در دسترس است یا خیر. پس از این مرحله نمی توان آن را تغییر داد و در طول مدت پروژه باقی می ماند.
  • برای اطلاع شما، یک مقدار سوم وجود دارد، یک شماره پروژه ، که برخی از API ها از آن استفاده می کنند. در مورد هر سه این مقادیر در مستندات بیشتر بیاموزید.
  1. در مرحله بعد، برای استفاده از منابع Cloud/APIها باید صورتحساب را در کنسول Cloud فعال کنید . اجرا کردن از طریق این کد لبه هزینه زیادی نخواهد داشت. برای خاموش کردن منابع برای جلوگیری از تحمیل صورت‌حساب فراتر از این آموزش، می‌توانید منابعی را که ایجاد کرده‌اید حذف کنید یا پروژه را حذف کنید. کاربران جدید Google Cloud واجد شرایط برنامه آزمایشی رایگان 300 دلاری هستند.

Cloud Shell را راه اندازی کنید

در حالی که Google Cloud را می توان از راه دور از لپ تاپ شما کار کرد، در این کد لبه از Google Cloud Shell استفاده خواهید کرد، یک محیط خط فرمان که در Cloud اجرا می شود.

از Google Cloud Console ، روی نماد Cloud Shell در نوار ابزار بالا سمت راست کلیک کنید:

84688aa223b1c3a2.png

تهیه و اتصال به محیط فقط چند لحظه طول می کشد. وقتی تمام شد، باید چیزی شبیه به این را ببینید:

320e18fedb7fbe0.png

این ماشین مجازی با تمام ابزارهای توسعه که شما نیاز دارید بارگذاری شده است. این یک فهرست اصلی 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 بروید:

409982568cebdbf8.png

در اسکرین شات بالا، مجموعه 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، باید تابع ظاهر شود:

c910875d4dc0aaa8.png

در خروجی استقرار، باید بتوانید URL تابع خود را ببینید که از یک قرارداد نامگذاری خاصی پیروی می کند ( https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME} ) و البته، شما همچنین می توانید این نشانی اینترنتی راه انداز HTTP را در رابط کاربری Cloud Console، در تب trigger پیدا کنید:

380ffc46eb56441e.png

همچنین می توانید 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 and 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 ) برگردید. ما فیلدهای کتاب را از محموله ارسال شده در متن درخواست بازیابی می کنیم. سپس جزئیات کتاب را در 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 ما اکنون در لیست ظاهر می‌شود:

f62fbca02a8127c0.png

آخرین مرحله ای که در اینجا انجام خواهیم داد، بازیابی 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 استفاده خواهیم کرد:

  • یک کادر انتخاب برای انتخاب زبان کتاب:

6fb9f741000a2dc1.png

  • یک جزء کارت برای نمایش جزئیات مربوط به یک کتاب خاص (از جمله یک بارکد برای نشان دادن ISBN کتاب، با استفاده از کتابخانه JsBarcode ):

3aa21a9e16e3244e.png

  • و یک دکمه برای بارگیری کتاب های بیشتر از پایگاه داده:

3925ad81c91bbac9.png

هنگامی که تمام آن اجزای بصری را با هم ترکیب می کنیم، صفحه وب به دست آمده برای مرور کتابخانه ما به صورت زیر خواهد بود:

18a5117150977d6.png

فایل پیکربندی 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 استفاده می کنیم ، از ویژگی زمان واقعی خود برای به روزرسانی داده های کتاب نمایش داده شده در هنگام ایجاد تغییرات استفاده می کنیم.