1. סקירה כללית
המטרה של ה-Codelab הזה היא לצבור ניסיון בשירותים 'ללא שרת' שמוצעים על ידי Google Cloud Platform:
- Cloud Functions – כדי לפרוס יחידות קטנות של לוגיקה עסקית בצורת פונקציות שמגיבות לאירועים שונים (הודעות Pub/Sub, קבצים חדשים ב-Cloud Storage, בקשות HTTP ועוד).
- App Engine – לפריסה ולהצגה של אפליקציות אינטרנט, ממשקי API לאינטרנט, נקודות קצה עורפיות בנייד ונכסים סטטיים, עם יכולות מהירות של הגדלה והקטנה של נפח האחסון.
- Cloud Run – לפריסה ולשינוי גודל של קונטיינרים, שיכולים להכיל כל שפה, זמן ריצה או ספריה.
בנוסף, תלמדו איך להשתמש בשירותים בלי שרת (serverless) כדי לפרוס ולהרחיב ממשקי API של אינטרנט ו-API בארכיטקטורת REST, ותכירו כמה עקרונות טובים לעיצוב RESTful.
בסדנה הזו ניצור כלי לחיפוש במדף ספרים שכולל:
- פונקציית Cloud Functions: לייבוא מערך הנתונים הראשוני של הספרים שזמינים בספרייה שלנו, במסד הנתונים של מסמכים ב-Cloud Firestore.
- קונטיינר Cloud Run: שיחשוף API בארכיטקטורת REST על התוכן של מסד הנתונים שלנו,
- קצה קדמי לאינטרנט של App Engine: כדי לעיין ברשימת הספרים, על ידי קריאה ל-API בארכיטקטורת REST שלנו.
כך ייראה ממשק הקצה של האתר בסוף ה-Codelab הזה:

מה תלמדו
- Cloud Functions
- Cloud Firestore
- Cloud Run
- App Engine
2. הגדרה ודרישות
הגדרת סביבה בקצב אישי
- נכנסים ל-מסוף Google Cloud ויוצרים פרויקט חדש או משתמשים בפרויקט קיים. אם עדיין אין לכם חשבון Gmail או Google Workspace, אתם צריכים ליצור חשבון.



- שם הפרויקט הוא השם המוצג למשתתפים בפרויקט. זו מחרוזת תווים שלא נמצאת בשימוש ב-Google APIs. תמיד אפשר לעדכן את המיקום.
- מזהה הפרויקט הוא ייחודי לכל הפרויקטים ב-Google Cloud ואי אפשר לשנות אותו אחרי שהוא מוגדר. מסוף Cloud יוצר באופן אוטומטי מחרוזת ייחודית, ובדרך כלל לא צריך לדעת מה היא. ברוב ה-Codelabs, תצטרכו להפנות למזהה הפרויקט (בדרך כלל מסומן כ-
PROJECT_ID). אם אתם לא אוהבים את המזהה שנוצר, אתם יכולים ליצור מזהה אקראי אחר. אפשר גם לנסות שם משתמש משלכם ולבדוק אם הוא זמין. אי אפשר לשנות את ההגדרה הזו אחרי השלב הזה, והיא נשארת לאורך הפרויקט. - לידיעתכם, יש ערך שלישי, מספר פרויקט, שחלק מממשקי ה-API משתמשים בו. מידע נוסף על שלושת הערכים האלה מופיע במאמרי העזרה.
- בשלב הבא, תצטרכו להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבי Cloud או בממשקי API של Cloud. השלמת ה-codelab הזה לא תעלה לכם הרבה, אם בכלל. כדי להשבית את המשאבים ולמנוע חיובים נוספים אחרי שתסיימו את המדריך הזה, תוכלו למחוק את המשאבים שיצרתם או למחוק את הפרויקט. משתמשים חדשים ב-Google Cloud זכאים לתוכנית תקופת ניסיון בחינם בשווי 300$.
מפעילים את Cloud Shell
אפשר להפעיל את Google Cloud מרחוק מהמחשב הנייד, אבל ב-Codelab הזה נשתמש ב-Google Cloud Shell, סביבת שורת פקודה שפועלת בענן.
ב-מסוף Google Cloud, לוחצים על סמל Cloud Shell בסרגל הכלים שבפינה הימנית העליונה:

הקצאת המשאבים והחיבור לסביבה יימשכו רק כמה רגעים. בסיום התהליך, אמור להופיע משהו כזה:

המכונה הווירטואלית הזו כוללת את כל הכלים שדרושים למפתחים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר מאוד את הביצועים והאימות ברשת. אפשר לבצע את כל העבודה ב-codelab הזה בדפדפן. לא צריך להתקין שום דבר.
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}
בהמשך ה-Codelab הזה, כשנטמיע את API בארכיטקטורת REST, נצטרך למיין ולסנן את הנתונים. לשם כך, ניצור שלושה אינדקסים:
$ gcloud firestore indexes composite create --collection-group=books \
--field-config field-path=language,order=ascending \
--field-config field-path=updated,order=descending
$ gcloud firestore indexes composite create --collection-group=books \
--field-config field-path=author,order=ascending \
--field-config field-path=updated,order=descending
שלושת האינדקסים האלה תואמים לחיפושים שנבצע לפי מחבר או שפה, תוך שמירה על הסדר באוסף באמצעות שדה מעודכן.
4. קבל את הקוד
אפשר לקבל את הקוד ממאגר GitHub הבא:
$ git clone https://github.com/glaforge/serverless-web-apis
קוד האפליקציה נכתב באמצעות Node.JS.
מבנה התיקיות שרלוונטי לשיעור ה-Lab הזה הוא:
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 Functions:
[
{
"isbn": "9780435272463",
"author": "Chinua Achebe",
"language": "English",
"pages": 209,
"title": "Things Fall Apart",
"year": 1958
},
{
"isbn": "9781414251196",
"author": "Hans Christian Andersen",
"language": "Danish",
"pages": 784,
"title": "Fairy tales",
"year": 1836
},
...
]
כל רשומת ספר במערך הזה מכילה את הפרטים הבאים:
-
isbn– קוד ISBN-13 שמזהה את הספר. -
author— שם מחבר הספר. -
language— השפה המדוברת שבה הספר כתוב. -
pages– מספר העמודים בספר. -
title— שם הספר. -
year– השנה שבה הספר פורסם.
6. נקודת קצה של פונקציה לייבוא נתוני ספרים לדוגמה
בקטע הראשון הזה, נטמיע את נקודת הקצה שתשמש לייבוא נתונים לדוגמה של ספרים. לשם כך נשתמש ב-Cloud Functions.
עיון בקוד
נתחיל בבדיקה של הקובץ package.json:
{
"name": "function-import",
"description": "Import sample book data",
"license": "Apache-2.0",
"dependencies": {
"@google-cloud/firestore": "^4.9.9"
},
"devDependencies": {
"@google-cloud/functions-framework": "^3.1.0"
},
"scripts": {
"start": "npx @google-cloud/functions-framework --target=parseBooks"
}
}
בתלות בזמן הריצה, אנחנו צריכים רק את מודול @google-cloud/firestore NPM כדי לגשת למסד הנתונים ולאחסן את נתוני הספרים. מתחת לפני השטח, זמן הריצה של Cloud Functions מספק גם את מסגרת האינטרנט Express, כך שאין צורך להצהיר עליה כתלות.
בתלות בפיתוח, אנחנו מצהירים על Functions Framework (@google-cloud/functions-framework), שהוא מסגרת זמן הריצה שמשמשת להפעלת הפונקציות. זו מסגרת קוד פתוח שבה אפשר להשתמש גם באופן מקומי במחשב (במקרה שלנו, בתוך Cloud Shell) כדי להריץ פונקציות בלי לפרוס אותן בכל פעם שמבצעים שינוי, וכך לשפר את לולאת המשוב של הפיתוח.
כדי להתקין את יחסי התלות, משתמשים בפקודה install:
$ npm install
הסקריפט start משתמש ב-Functions Framework כדי לספק לכם פקודה שבה תוכלו להשתמש כדי להריץ את הפונקציה באופן מקומי באמצעות ההוראה הבאה:
$ npm start
כדי ליצור אינטראקציה עם הפונקציה, אפשר להשתמש ב-curl או בתצוגה המקדימה באינטרנט של Cloud Shell לבקשות HTTP GET.
עכשיו נסתכל על קובץ index.js שמכיל את הלוגיקה של פונקציית ייבוא נתוני הספרים:
const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');
אנחנו יוצרים מופע של מודול Firestore ומפנים אותו לאוסף הספרים (בדומה לטבלה במסדי נתונים יחסיים).
functions.http('parseBooks', async (req, resp) => {
if (req.method !== "POST") {
resp.status(405).send({error: "Only method POST allowed"});
return;
}
if (req.headers['content-type'] !== "application/json") {
resp.status(406).send({error: "Only application/json accepted"});
return;
}
...
})
אנחנו מייצאים את פונקציית ה-JavaScript parseBooks. זו הפונקציה שנצהיר עליה כשנפרוס אותה בהמשך.
בשלב הבא, צריך לוודא את הדברים הבאים:
- אנחנו מקבלים רק בקשות HTTP
POST, אחרת אנחנו מחזירים קוד סטטוס405כדי לציין שאסור להשתמש בשיטות HTTP אחרות. - אנחנו מקבלים רק מטען ייעודי (payload) מסוג
application/json, אחרת אנחנו שולחים קוד סטטוס406כדי לציין שזה לא פורמט מטען ייעודי (payload) קביל.
const books = req.body;
const writeBatch = firestore.batch();
for (const book of books) {
const doc = bookStore.doc(book.isbn);
writeBatch.set(doc, {
title: book.title,
author: book.author,
language: book.language,
pages: book.pages,
year: book.year,
updated: Firestore.Timestamp.now()
});
}
לאחר מכן, נוכל לאחזר את מטען ה-JSON באמצעות body של הבקשה. אנחנו מכינים פעולת אצווה ב-Firestore כדי לאחסן את כל הספרים בכמות גדולה. אנחנו מבצעים איטרציה על מערך ה-JSON שכולל את פרטי הספר, ועוברים על השדות isbn, title, author, language, pages ו-year. קוד ה-ISBN של הספר ישמש כמפתח ראשי או כמזהה שלו.
try {
await writeBatch.commit();
console.log("Saved books in Firestore");
} catch (e) {
console.error("Error saving books:", e);
resp.status(400).send({error: "Error saving books"});
return;
};
resp.status(202).send({status: "OK"});
אחרי שהרוב הגדול של הנתונים מוכן, אפשר לבצע את הפעולה. אם פעולת האחסון נכשלת, אנחנו מחזירים את קוד הסטטוס 400 כדי לציין שהיא נכשלה. אחרת, אנחנו יכולים להחזיר תשובה מסוג OK, עם קוד הסטטוס 202 שמציין שהבקשה לשמירה בכמות גדולה התקבלה.
הפעלה ובדיקה של פונקציית הייבוא
לפני הרצת הקוד, נתקין את יחסי התלות באמצעות הפקודה:
$ npm install
כדי להריץ את הפונקציה באופן מקומי, נשתמש בפקודת הסקריפט start שהגדרנו ב-package.json, הודות ל-Functions Framework:
$ npm start > start > npx @google-cloud/functions-framework --target=parseBooks Serving function... Function: parseBooks URL: http://localhost:8080/
כדי לשלוח בקשת HTTP POST לפונקציה המקומית, אפשר להריץ את הפקודה:
$ curl -d "@../data/books.json" \
-H "Content-Type: application/json" \
http://localhost:8080/
כשמריצים את הפקודה הזו, מוצג הפלט הבא שמאשר שהפונקציה פועלת באופן מקומי:
{"status":"OK"}
אפשר גם להיכנס לממשק המשתמש של Cloud Console כדי לוודא שהנתונים אכן מאוחסנים ב-Firestore:

בצילום המסך שלמעלה אפשר לראות את האוסף books שנוצר, את רשימת מסמכי הספר שזוהו לפי קוד ה-ISBN של הספר ואת הפרטים של רשומת הספר הספציפית הזו בצד שמאל.
פריסת הפונקציה בענן
כדי לפרוס את הפונקציה ב-Cloud Functions, נשתמש בפקודה הבאה בספרייה function-import:
$ gcloud functions deploy bulk-import \
--gen2 \
--trigger-http \
--runtime=nodejs20 \
--allow-unauthenticated \
--max-instances=30
--region=${REGION} \
--source=. \
--entry-point=parseBooks
אנחנו פורסים את הפונקציה עם שם סמלי של bulk-import. הפונקציה הזו מופעלת באמצעות בקשות HTTP. אנחנו משתמשים בזמן הריצה Node.JS 20. אנחנו פורסים את הפונקציה באופן ציבורי (מומלץ לאבטח את נקודת הקצה הזו). מציינים את האזור שבו רוצים שהפונקציה תהיה. אנחנו מצביעים על המקורות בספרייה המקומית ומשתמשים ב-parseBooks (פונקציית JavaScript המיוצאת) כנקודת כניסה.
אחרי כמה דקות או פחות, הפונקציה נפרסת בענן. בממשק המשתמש של מסוף Cloud, הפונקציה אמורה להופיע:

בפלט הפריסה אמורה להופיע כתובת ה-URL של הפונקציה, שפועלת לפי מוסכמת שמות מסוימת (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}). כמובן שאפשר למצוא את כתובת ה-URL הזו של טריגר ה-HTTP גם בממשק המשתמש של מסוף Cloud, בכרטיסייה 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"}
עכשיו פונקציית הייבוא שלנו פרוסה ומוכנה, והעלינו את נתוני הדוגמה. הגיע הזמן לפתח את API בארכיטקטורת REST שחושף את קבוצת הנתונים הזו.
7. הסכם בנושא API בארכיטקטורת REST
למרות שאנחנו לא מגדירים חוזה API באמצעות, לדוגמה, מפרט Open API, אנחנו נבדוק את נקודות הקצה השונות של API בארכיטקטורת REST.
ה-API מחליף אובייקטים של JSON של ספרים, שכוללים:
-
isbn(אופציונלי) – קודStringתקין של ISBN באורך 13 תווים, -
author– מחרוזת לא ריקהStringשמייצגת את שם מחבר הספר, -
language– מחרוזת לא ריקהStringשמכילה את השפה שבה הספר נכתב, -
pages— מספר חיוביIntegerשל מספר הדפים בספר, -
title—Stringלא ריק עם שם הספר, -
year– ערךIntegerשל שנת הפרסום של הספר.
מטען ייעודי לדוגמה של ספר:
{
"isbn": "9780435272463",
"author": "Chinua Achebe",
"language": "English",
"pages": 209,
"title": "Things Fall Apart",
"year": 1958
}
GET /books
לקבל את רשימת כל הספרים, שאפשר לסנן לפי מחבר או שפה, ולחלק אותה לדפים של 10 תוצאות בכל פעם.
מטען ייעודי (payload) של הגוף: אין.
פרמטרים של שאילתה:
-
author(אופציונלי) – סינון רשימת הספרים לפי מחבר, -
language(אופציונלי) – סינון רשימת הספרים לפי שפה, -
page(אופציונלי, ברירת מחדל = 0) – מציין את הדירוג של דף התוצאות שיוחזר.
הפונקציה מחזירה: מערך JSON של אובייקטים של ספרים.
קודי סטטוס:
-
200– כשהבקשה מצליחה לאחזר את רשימת הספרים, -
400– אם מתרחשת שגיאה.
POST /books ו-POST /books/{isbn}
שליחת מטען ייעודי (payload) של ספר חדש, עם isbn פרמטר של הנתיב (במקרה כזה לא צריך לציין את isbn הקוד במטען הייעודי של הספר) או בלי (במקרה כזה צריך לציין את isbn הקוד במטען הייעודי של הספר)
מטען ייעודי (Payload) של גוף הבקשה: אובייקט ספר.
פרמטרים של שאילתה: אין.
החזרות: לא מבוצעת פעולה.
קודי סטטוס:
-
201– כשהספר נשמר בהצלחה, -
406— אם הקודisbnלא תקין, -
400– אם מתרחשת שגיאה.
GET /books/{isbn}
הפונקציה מאחזרת ספר מהספרייה, שמזוהה באמצעות קוד isbn שלו, שמועבר כפרמטר של הנתיב.
מטען ייעודי (payload) של הגוף: אין.
פרמטרים של שאילתה: אין.
הפונקציה מחזירה: אובייקט JSON של ספר, או אובייקט שגיאה אם הספר לא קיים.
קודי סטטוס:
-
200— אם הספר נמצא במסד הנתונים, 400— אם מתרחשת שגיאה,-
404— אם לא ניתן היה למצוא את הספר, -
406— אם קודisbnלא תקין.
PUT /books/{isbn}
מעדכן ספר קיים, שמזוהה על ידי isbn שמועבר כפרמטר של הנתיב.
מטען ייעודי (Payload) של גוף הבקשה: אובייקט ספר. אפשר להעביר רק את השדות שצריך לעדכן, והשדות האחרים הם אופציונליים.
פרמטרים של שאילתה: אין.
הפונקציה מחזירה את הספר המעודכן.
קודי סטטוס:
-
200– כשהספר מתעדכן בהצלחה, 400— אם מתרחשת שגיאה,-
406— אם קודisbnלא תקין.
DELETE /books/{isbn}
מוחק ספר קיים, שמזוהה על ידי isbn שמועבר כפרמטר של הנתיב.
מטען ייעודי (payload) של הגוף: אין.
פרמטרים של שאילתה: אין.
החזרות: לא מבוצעת פעולה.
קודי סטטוס:
-
204– כשהספר נמחק בהצלחה, -
400– אם מתרחשת שגיאה.
8. פריסה של API בארכיטקטורת REST בקונטיינר וחשיפתו
עיון בקוד
Dockerfile
נתחיל בבדיקת הקובץ Dockerfile, שיהיה אחראי על יצירת קונטיינר לקוד האפליקציה שלנו:
FROM node:20-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . ./
CMD [ "node", "index.js" ]
אנחנו משתמשים בתמונה של Node.JS 20 slim. אנחנו עובדים בספרייה /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 שלנו באינטרנט תלויה ב:
- מודול ה-NPM של Firestore כדי לגשת לנתוני הספר במסד הנתונים,
- ספריית
corsלטיפול בבקשות CORS (שיתוף משאבים בין מקורות), כי 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 כ-framework לאינטרנט כדי להטמיע את ה-API בארכיטקטורת REST שלנו. אנחנו משתמשים במודול 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 ותגיב עם קוד סטטוס 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);
יכול להיות שהלוגיקה כאן תיראה קצת מורכבת בהתחלה, אבל מה שאנחנו עושים זה להוסיף קישור previous אם אנחנו לא בדף הראשון של הנתונים. אם הדף מלא בנתונים (כלומר, מכיל את המספר המקסימלי של ספרים שמוגדר על ידי הקבוע PAGE_SIZE, בהנחה שיש עוד דף עם נתונים), אנחנו מוסיפים קישור הבא. לאחר מכן אנחנו משתמשים בפונקציה resource#links() של Express כדי ליצור את הכותרת הנכונה עם התחביר הנכון.
למען הידע הכללי, ה-header של הקישור ייראה בערך כך:
link: </books?page=1>; rel="prev", </books?page=3>; rel="next"
-
POST /booksו-POST /books/:isbn
שתי נקודות הקצה האלה משמשות ליצירת ספר חדש. באחד מהם מועבר קוד ה-ISBN במטען הייעודי (payload) של הספר, ובשני הוא מועבר כפרמטר של הנתיב. בכל מקרה, שתי הפונקציות קוראות לפונקציה 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). אנחנו מאחזרים את שדות הספר מהמטען הייעודי (payload) שמועבר בגוף הבקשה. לאחר מכן נאחסן את פרטי הספר ב-Firestore. הערך 201 מוחזר אם הפעולה הצליחה, והערך 400 מוחזר אם הפעולה נכשלה.
כשחוזרת תשובה חיובית, אנחנו גם מגדירים את כותרת המיקום, כדי לספק ללקוח של ה-API רמזים לגבי המיקום של המשאב שנוצר לאחרונה. הכותרת תיראה כך:
Location: /books/9781234567898
GET /books/:isbn
נביא ספר מ-Firestore, שמזוהה באמצעות ה-ISBN שלו.
app.get('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
const docSnapshot = await docRef.get();
if (!docSnapshot.exists) {
console.log(`Book not found ${parsedIsbn.isbn13}`)
res.status(404)
.send({error: `Could not find book ${parsedIsbn.isbn13}`});
return;
}
console.log(`Fetched book ${parsedIsbn.isbn13}`, docSnapshot.data());
const {title, author, pages, year, language, ...otherFields} = docSnapshot.data();
const book = {isbn: parsedIsbn.isbn13, title, author, pages, year, language};
res.status(200).send(book);
} catch (e) {
console.error(`Failed to fetch book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to fetch book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
כמו תמיד, אנחנו בודקים אם מספר ה-ISBN תקין. אנחנו שולחים שאילתה ל-Firestore כדי לאחזר את הספר. המאפיין snapshot.exists שימושי כדי לדעת אם ספר אכן נמצא. אחרת, אנחנו מחזירים שגיאה וקוד סטטוס 404 Not Found. אנחנו מאחזרים את נתוני הספר ויוצרים אובייקט JSON שמייצג את הספר, כדי להחזיר אותו.
PUT /books/:isbn
אנחנו משתמשים בשיטת PUT כדי לעדכן ספר קיים.
app.put('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.set({
...req.body,
updated: Firestore.Timestamp.now()
}, {merge: true});
console.log(`Updated book ${parsedIsbn.isbn13}`);
res.status(201)
.location(`/books/${parsedIsbn.isbn13}`)
.send({status: `Book ${parsedIsbn.isbn13} updated`});
} catch (e) {
console.error(`Failed to update book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to update book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
אנחנו מעדכנים את updated שדה התאריך/השעה כדי לזכור מתי עדכנו לאחרונה את הרשומה הזו. אנחנו משתמשים באסטרטגיה {merge:true} שמחליפה שדות קיימים בערכים החדשים שלהם (אחרת, כל השדות מוסרים ורק השדות החדשים במטען הייעודי (payload) נשמרים, וכך נמחקים שדות קיימים מהעדכון הקודם או מהיצירה הראשונית).
הגדרנו גם את הכותרת Location כך שתצביע על ה-URI של הספר.
DELETE /books/:isbn
מחיקת ספרים היא פשוטה למדי. פשוט קוראים לשיטה delete() בהפניה למסמך. אנחנו מחזירים קוד סטטוס 204, כי אנחנו לא מחזירים תוכן.
app.delete('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.delete();
console.log(`Book ${parsedIsbn.isbn13} was deleted`);
res.status(204).end();
} catch (e) {
console.error(`Failed to delete book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to delete book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
הפעלת שרת Express / Node
לבסוף, מפעילים את השרת, שמאזין ליציאה 8080 כברירת מחדל:
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Books Web API service: listening on port ${port}`);
console.log(`Node ${process.version}`);
});
הרצת האפליקציה באופן מקומי
כדי להריץ את האפליקציה באופן מקומי, קודם נתקין את התלויות באמצעות הפקודה:
$ npm install
ואז אפשר להתחיל עם:
$ npm start
השרת יתחיל לפעול ב-localhost ויאזין ליציאה 8080 כברירת מחדל.
אפשר גם ליצור קונטיינר Docker ולהריץ את קובץ האימג' של הקונטיינר באמצעות הפקודות הבאות:
$ docker build -t crud-web-api . $ docker run --rm -p 8080:8080 -it crud-web-api
הפעלה ב-Docker היא גם דרך מצוינת לוודא שהקונטיינר של האפליקציה יפעל בצורה תקינה בזמן שאנחנו יוצרים אותו בענן באמצעות Cloud Build.
בדיקת ה-API
לא משנה איך מריצים את קוד API בארכיטקטורת REST (ישירות דרך Node או דרך קובץ אימג' של קונטיינר של Docker), עכשיו אפשר להריץ כמה שאילתות נגדו.
- יצירת ספר חדש (מספר ISBN בגוף מטען הייעודי):
$ curl -XPOST -d '{"isbn":"9782070368228","title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
-H "Content-Type: application/json" \
http://localhost:8080/books
- יצירת ספר חדש (מספר ISBN בפרמטר של נתיב):
$ curl -XPOST -d '{"title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
-H "Content-Type: application/json" \
http://localhost:8080/books/9782070368228
- מחיקת ספר (הספר שיצרנו):
$ curl -XDELETE http://localhost:8080/books/9782070368228
- אחזור ספר לפי ISBN:
$ curl http://localhost:8080/books/9780140449136 $ curl http://localhost:8080/books/9782070360536
- כדי לעדכן ספר קיים רק על ידי שינוי השם שלו:
$ curl -XPUT \
-d '{"title":"Book"}' \
-H "Content-Type: application/json" \
http://localhost:8080/books/9780003701203
- אחזור רשימת הספרים (ה-10 הראשונים):
$ curl http://localhost:8080/books
- חיפוש ספרים שנכתבו על ידי מחבר מסוים:
$ curl http://localhost:8080/books?author=Virginia+Woolf
- תציג רשימה של הספרים שנכתבו באנגלית:
$ curl http://localhost:8080/books?language=English
- טעינת הדף הרביעי של הספרים:
$ curl http://localhost:8080/books?page=3
אפשר גם לשלב בין פרמטרים של שאילתות author, language ו-books כדי לחדד את החיפוש.
פיתוח ופריסת ה-API בארכיטקטורת REST בקונטיינרים
אנחנו מרוצים מפעולת ה-API בארכיטקטורת REST בהתאם לתוכנית, ולכן זה הזמן הנכון לפרוס אותו ב-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 שהשירות 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 API בארכיטקטורת REST, כי קוד הקצה הקדמי של App Engine יקיים אינטראקציה עם ה-API.
9. אירוח אפליקציית אינטרנט כדי לגלוש בספרייה
השלב האחרון בפרויקט הוא ליצור ממשק קצה לאתר שיתקשר עם ה-API ל-REST. לשם כך, נשתמש ב-Google App Engine, עם קוד JavaScript של לקוח שיקרא ל-API באמצעות בקשות AJAX (באמצעות Fetch API בצד הלקוח).
האפליקציה שלנו, למרות שהיא נפרסה בסביבת זמן הריצה של Node.JS App Engine, מורכבת בעיקר ממשאבים סטטיים. אין הרבה קוד בקצה העורפי, כי רוב האינטראקציות של המשתמשים יתבצעו בדפדפן באמצעות JavaScript בצד הלקוח. לא נשתמש בשום מסגרת JavaScript מתוחכמת של חזית האתר, אלא רק ב-JavaScript "רגיל", עם כמה רכיבי אינטרנט לממשק המשתמש באמצעות ספריית רכיבי האינטרנט Shoelace:
- תיבת בחירה לבחירת שפת הספר:

- רכיב כרטיס להצגת הפרטים של ספר מסוים (כולל ברקוד שמייצג את מספר ה-ISBN של הספר, באמצעות הספרייה JsBarcode):

- וכפתור לטעינת ספרים נוספים מהמסד הנתונים:

כשמשלבים את כל הרכיבים החזותיים האלה, דף האינטרנט שמתקבל לגלישה בספרייה שלנו נראה כך:

app.yaml קובץ התצורה
נתחיל להתעמק בבסיס הקוד של אפליקציית App Engine הזו, ונבחן את קובץ ההגדרות app.yaml שלה. זהו קובץ שספציפי ל-App Engine, ומאפשר להגדיר דברים כמו משתני סביבה, רכיבי handler שונים של האפליקציה או לציין שחלק מהמשאבים הם נכסים סטטיים, שיוצגו על ידי ה-CDN המובנה של App Engine.
runtime: nodejs14
env_variables:
RUN_CRUD_SERVICE_URL: CHANGE_ME
handlers:
- url: /js
static_dir: public/js
- url: /css
static_dir: public/css
- url: /img
static_dir: public/img
- url: /(.+\.html)
static_files: public/html/\1
upload: public/(.+\.html)
- url: /
static_files: public/html/index.html
upload: public/html/index\.html
- url: /.*
secure: always
script: auto
אנחנו מציינים שהאפליקציה שלנו היא Node.JS, ושאנחנו רוצים להשתמש בגרסה 14.
לאחר מכן מגדירים משתנה סביבה שמצביע על כתובת ה-URL של שירות Cloud Run. צריך לעדכן את ה-placeholder CHANGE_ME עם כתובת ה-URL הנכונה (בהמשך מוסבר איך לעשות את זה).
לאחר מכן, מגדירים מטפלים שונים. שלושת הראשונים מצביעים על המיקום של קוד ה-HTML, ה-CSS וה-JavaScript בצד הלקוח, בתיקייה public/ ובתיקיות המשנה שלה. השורה הרביעית מציינת שכתובת ה-URL הבסיסית של אפליקציית App Engine צריכה להפנות לדף index.html. כך לא נראה את הסיומת index.html בכתובת ה-URL כשניגשים לשורש של האתר. האחרון הוא ברירת המחדל, שינתב את כל כתובות ה-URL האחרות (/.*) לאפליקציית Node.JS שלנו (כלומר, החלק "הדינמי" של האפליקציה, בניגוד לנכסים הסטטיים שתיארנו).
עכשיו נעדכן את כתובת ה-URL של Web API בשירות Cloud Run.
בספרייה appengine-frontend/, מריצים את הפקודה הבאה כדי לעדכן את משתנה הסביבה שמצביע על כתובת ה-URL של 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
הקובץ package.json של Node.JS
{
"name": "appengine-frontend",
"description": "Web frontend",
"license": "Apache-2.0",
"main": "index.js",
"engines": {
"node": "^14.0.0"
},
"dependencies": {
"express": "^4.17.1",
"isbn3": "^1.1.10"
},
"devDependencies": {
"nodemon": "^2.0.7"
},
"scripts": {
"start": "node index.js",
"dev": "nodemon --watch server --inspect index.js"
}
}
אנחנו חוזרים ומדגישים שאנחנו רוצים להריץ את האפליקציה הזו באמצעות Node.JS 14. אנחנו מסתמכים על מסגרת Express, וגם על מודול isbn3 NPM לאימות קודי ISBN של ספרים.
בתלות בפיתוח, נשתמש במודול nodemon כדי לעקוב אחרי שינויים בקבצים. אמנם אפשר להריץ את האפליקציה באופן מקומי באמצעות npm start, לבצע שינויים בקוד, לעצור את האפליקציה באמצעות ^C ואז להפעיל אותה מחדש, אבל זה קצת מייגע. במקום זאת, אפשר להשתמש בפקודה הבאה כדי שהאפליקציה תיטען מחדש או תופעל מחדש באופן אוטומטי כשיש שינויים:
$ npm run dev
index.js קוד Node.JS
const express = require('express');
const app = express();
app.use(express.static('public'));
const bodyParser = require('body-parser');
app.use(bodyParser.json());
אנחנו דורשים את מסגרת האינטרנט Express. אנחנו מציינים שהספרייה הציבורית מכילה נכסים סטטיים שאפשר להציג (לפחות כשהאפליקציה פועלת באופן מקומי במצב פיתוח) באמצעות תוכנת הביניים static. לבסוף, אנחנו דורשים body-parser כדי לנתח את מטען ה-JSON שלנו.
בואו נסתכל על שני המסלולים שהגדרנו:
app.get('/', async (req, res) => {
res.redirect('/html/index.html');
});
app.get('/webapi', async (req, res) => {
res.send(process.env.RUN_CRUD_SERVICE_URL);
});
הראשון שמתאים ל-/ יפנה ל-index.html בספרייה public/html שלנו. במצב פיתוח, אנחנו לא מריצים בתוך זמן הריצה של App Engine, ולכן לא מתבצע ניתוב של כתובות URL של App Engine. לכן, במקום זאת, אנחנו פשוט מפנים כאן את כתובת ה-URL הבסיסית לקובץ ה-HTML.
נקודת הקצה השנייה שאנחנו מגדירים /webapi תחזיר את כתובת ה-URL של Cloud RUN API בארכיטקטורת 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">
בשתי השורות הראשונות מתבצע ייבוא של ספריית רכיבי האינטרנט של Shoelace (סקריפט וגיליון סגנונות).
בשורה הבאה מתבצע ייבוא של ספריית JsBarcode, כדי ליצור את הברקודים של קודי ה-ISBN של הספר.
השורות האחרונות מייבאות את קוד ה-JavaScript ואת גיליון הסגנונות (CSS) שלנו, שנמצאים בספריות המשנה public/ שלנו.
ב-body של דף ה-HTML, אנחנו משתמשים ברכיבי Shoelace עם תגי הרכיבים המותאמים אישית שלהם, כמו:
<sl-icon name="book-half"></sl-icon>
...
<sl-select id="language-select" placeholder="Select a language..." clearable>
<sl-menu-item value="English">English</sl-menu-item>
<sl-menu-item value="French">French</sl-menu-item>
...
</sl-select>
...
<sl-button id="more-button" type="primary" size="large">
More books...
</sl-button>
...
אנחנו משתמשים גם בתבניות HTML וביכולת שלהן למילוי משבצות כדי לייצג ספר. אנחנו ניצור עותקים של התבנית הזו כדי לאכלס את רשימת הספרים, ונחליף את הערכים במשבצות בפרטי הספרים:
<template id="book-card">
<sl-card class="card-overview">
...
<slot name="author">Author</slot>
...
</sl-card>
</template>
מספיק HTML, אנחנו כמעט מסיימים את הבדיקה של הקוד. נותר עוד חלק חשוב אחד: קוד app.js JavaScript בצד הלקוח שמתקשר עם ה-API בארכיטקטורת REST שלנו.
קוד JavaScript בצד הלקוח של app.js
מתחילים עם רכיב event listener ברמה העליונה שמחכה לטעינה של תוכן ה-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 של API בארכיטקטורת REST, באמצעות קוד הצומת של App Engine שמחזיר את משתנה הסביבה שהגדרנו בהתחלה ב-app.yaml. בזכות משתנה הסביבה, נקודת הקצה /webapi, שנקראת מקוד JavaScript בצד הלקוח, לא היינו צריכים לכתוב בתוך הקוד את כתובת ה-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(), ומעבירים את כתובת ה-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 המדויקת שבה צריך להשתמש כדי לקרוא ל-API בארכיטקטורת 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);
...
}
}
בחלק של הפונקציה שמופיע למעלה, לכל ספר שמוחזר על ידי API בארכיטקטורת 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...]. - בוחרים שפה מסוימת כדי לראות רק ספרים בשפה הזו.
- כדי לחזור לרשימה של כל הספרים, אפשר ללחוץ על סימן ה-X הקטן בתיבת הבחירה כדי לבטל את הבחירה.
10. ניקוי (אופציונלי)
אם אתם לא מתכוונים להשאיר את האפליקציה, אתם יכולים למחוק את הפרויקט כולו כדי לחסוך בעלויות ולשמור על סביבת ענן נקייה:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
11. מעולה!
יצרנו קבוצה של שירותים באמצעות Cloud Functions, App Engine ו-Cloud Run כדי לחשוף נקודות קצה שונות של Web API וממשק קצה לאינטרנט, כדי לאחסן, לעדכן ולעיין בספרייה של ספרים, תוך שימוש בדפוסי עיצוב טובים לפיתוח REST API.
מה נכלל
- Cloud Functions
- Cloud Firestore
- Cloud Run
- App Engine
מידע נוסף
אם רוצים להרחיב את הדוגמה הקונקרטית הזו, הנה רשימה של דברים שכדאי לבדוק:
- כדאי להשתמש ב-API Gateway כדי לספק חזית API משותפת לפונקציית ייבוא הנתונים ולמאגר של API בארכיטקטורת REST, כדי להוסיף תכונות כמו טיפול במפתחות API לגישה ל-API, או כדי להגדיר הגבלות על קצב השימוש עבור צרכני API.
- פורסים את מודול הצומת Swagger-UI באפליקציית App Engine כדי לתעד את API בארכיטקטורת REST ולהציע סביבת בדיקה.
- בממשק הקצה, מעבר ליכולת הגלישה הקיימת, מוסיפים מסכים נוספים לעריכת הנתונים וליצירת רשומות חדשות של ספרים. בנוסף, מכיוון שאנחנו משתמשים במסד הנתונים Cloud Firestore, אנחנו יכולים להשתמש בתכונה בזמן אמת כדי לעדכן את נתוני הספר שמוצגים בזמן שמתבצעים שינויים.