1. ภาพรวม
เป้าหมายของ Codelab นี้คือการได้รับประสบการณ์การใช้งานบริการแบบ "Serverless" ที่ Google Cloud Platform มีให้บริการ
- Cloud Functions - เพื่อทำให้ใช้งานได้หน่วยตรรกะทางธุรกิจขนาดเล็กในรูปแบบของฟังก์ชันที่ตอบสนองต่อเหตุการณ์ต่างๆ (ข้อความ Pub/Sub, ไฟล์ใหม่ใน Cloud Storage, คำขอ HTTP และอื่นๆ)
- App Engine - เพื่อทำให้เว็บแอป, Web API, แบ็กเอนด์ของอุปกรณ์เคลื่อนที่, เนื้อหาสื่อแบบคงที่ใช้งานได้ และให้บริการ พร้อมความสามารถในการเพิ่มและลดทรัพยากรได้อย่างรวดเร็ว
- Cloud Run - เพื่อทำให้ใช้งานได้และปรับขนาดคอนเทนเนอร์ซึ่งมีภาษา รันไทม์ หรือไลบรารีใดก็ได้
และเพื่อดูวิธีใช้ประโยชน์จากบริการแบบ Serverless เหล่านั้นในการทำให้ใช้งานได้และปรับขนาด Web และ REST API พร้อมทั้งดูหลักการออกแบบ RESTful ที่ดีไปพร้อมกัน
ในเวิร์กช็อปนี้ เราจะสร้างโปรแกรมสำรวจชั้นวางหนังสือซึ่งประกอบด้วย
- Cloud Function: เพื่อนำเข้าชุดข้อมูลเริ่มต้นของหนังสือที่มีในคลังของเราในฐานข้อมูลเอกสาร Cloud Firestore
- คอนเทนเนอร์ Cloud Run: ที่จะแสดง REST API ผ่านเนื้อหาของฐานข้อมูล
- ฟรอนท์เอนด์ของเว็บ App Engine: เพื่อเรียกดูรายการหนังสือโดยการเรียก REST API ของเรา
ส่วนหน้าเว็บจะมีลักษณะดังนี้เมื่อสิ้นสุด Codelab นี้

สิ่งที่คุณจะได้เรียนรู้
- Cloud Functions
- Cloud Firestore
- Cloud Run
- App Engine
2. การตั้งค่าและข้อกำหนด
การตั้งค่าสภาพแวดล้อมแบบเรียนรู้ด้วยตนเอง
- ลงชื่อเข้าใช้ Google Cloud Console แล้วสร้างโปรเจ็กต์ใหม่หรือใช้โปรเจ็กต์ที่มีอยู่ซ้ำ หากยังไม่มีบัญชี Gmail หรือ Google Workspace คุณต้องสร้างบัญชี



- ชื่อโปรเจ็กต์คือชื่อที่แสดงสำหรับผู้เข้าร่วมโปรเจ็กต์นี้ ซึ่งเป็นสตริงอักขระที่ Google APIs ไม่ได้ใช้ คุณอัปเดตได้ทุกเมื่อ
- รหัสโปรเจ็กต์จะไม่ซ้ำกันในโปรเจ็กต์ Google Cloud ทั้งหมดและเปลี่ยนแปลงไม่ได้ (เปลี่ยนไม่ได้หลังจากตั้งค่าแล้ว) Cloud Console จะสร้างสตริงที่ไม่ซ้ำกันโดยอัตโนมัติ ซึ่งโดยปกติแล้วคุณไม่จำเป็นต้องสนใจว่าสตริงนั้นคืออะไร ใน Codelab ส่วนใหญ่ คุณจะต้องอ้างอิงรหัสโปรเจ็กต์ (โดยทั่วไปจะระบุเป็น
PROJECT_ID) หากไม่ชอบรหัสที่สร้างขึ้น คุณอาจสร้างรหัสแบบสุ่มอีกรหัสหนึ่งได้ หรือคุณอาจลองใช้ชื่อของคุณเองและดูว่ามีชื่อนั้นหรือไม่ คุณจะเปลี่ยนแปลงรหัสนี้หลังจากขั้นตอนนี้ไม่ได้ และรหัสจะคงอยู่ตลอดระยะเวลาของโปรเจ็กต์ - โปรดทราบว่ายังมีค่าที่ 3 ซึ่งคือหมายเลขโปรเจ็กต์ที่ API บางตัวใช้ ดูข้อมูลเพิ่มเติมเกี่ยวกับค่าทั้ง 3 นี้ได้ในเอกสารประกอบ
- จากนั้นคุณจะต้องเปิดใช้การเรียกเก็บเงินใน Cloud Console เพื่อใช้ทรัพยากร/API ของ Cloud การทำตาม Codelab นี้จะไม่มีค่าใช้จ่ายมากนัก หรืออาจไม่มีค่าใช้จ่ายเลย หากต้องการปิดทรัพยากรเพื่อหลีกเลี่ยงการเรียกเก็บเงินนอกเหนือจากบทแนะนำนี้ คุณสามารถลบทรัพยากรที่สร้างขึ้นหรือลบโปรเจ็กต์ได้ ผู้ใช้ Google Cloud รายใหม่มีสิทธิ์เข้าร่วมโปรแกรมช่วงทดลองใช้ฟรีมูลค่า$300 USD
เริ่มต้น Cloud Shell
แม้ว่าคุณจะใช้งาน Google Cloud จากระยะไกลจากแล็ปท็อปได้ แต่ใน Codelab นี้คุณจะใช้ Google Cloud Shell ซึ่งเป็นสภาพแวดล้อมบรรทัดคำสั่งที่ทำงานในระบบคลาวด์
จาก Google Cloud Console ให้คลิกไอคอน Cloud Shell ในแถบเครื่องมือด้านขวาบน

การจัดสรรและเชื่อมต่อกับสภาพแวดล้อมจะใช้เวลาเพียงไม่กี่นาที เมื่อเสร็จแล้ว คุณควรเห็นข้อความคล้ายกับตัวอย่างต่อไปนี้

เครื่องเสมือนนี้มาพร้อมเครื่องมือพัฒนาซอฟต์แวร์ทั้งหมดที่คุณต้องการ โดยมีไดเรกทอรีหลักแบบถาวรขนาด 5 GB และทำงานบน Google Cloud ซึ่งช่วยเพิ่มประสิทธิภาพเครือข่ายและการตรวจสอบสิทธิ์ได้อย่างมาก คุณสามารถทำงานทั้งหมดใน Codelab นี้ได้ภายในเบราว์เซอร์ คุณไม่จำเป็นต้องติดตั้งอะไร
3. เตรียมสภาพแวดล้อมและเปิดใช้ Cloud 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 นี้ เมื่อติดตั้งใช้งาน REST API เราจะต้องจัดเรียงและกรองข้อมูล ด้วยเหตุนี้ เราจึงจะสร้างดัชนี 3 รายการ ได้แก่
$ 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 Firestoreappengine-frontend— เว็บแอปพลิเคชัน App Engine นี้จะแสดงฟรอนท์เอนด์แบบอ่านอย่างเดียวที่เรียบง่ายเพื่อเรียกดูรายการหนังสือ
5. ข้อมูลคลังหนังสือตัวอย่าง
ในโฟลเดอร์ข้อมูล เรามีไฟล์ books.json ซึ่งมีรายชื่อหนังสือ 100 เล่มที่น่าอ่าน เอกสาร 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"
}
}
ในทรัพยากร Dependency ของรันไทม์ เราต้องการเพียงโมดูล @google-cloud/firestoreNPM เพื่อเข้าถึงฐานข้อมูลและจัดเก็บข้อมูลหนังสือ เบื้องหลังรันไทม์ของ Cloud Functions ยังมีเฟรมเวิร์กเว็บ Express ด้วย ดังนั้นเราจึงไม่จำเป็นต้องประกาศเป็นทรัพยากร Dependency
ในส่วนการขึ้นต่อกันในการพัฒนา เราจะประกาศ 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 และชี้ไปที่คอลเล็กชัน books (คล้ายกับตารางในฐานข้อมูลเชิงสัมพันธ์)
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 นี่คือฟังก์ชันที่เราจะประกาศเมื่อเราติดตั้งใช้งานในภายหลัง
คำสั่งถัดไป 2 รายการจะตรวจสอบว่า
- เรายอมรับเฉพาะคำขอ HTTP
POSTและจะแสดงรหัสสถานะ405เพื่อระบุว่าไม่อนุญาตให้ใช้วิธี HTTP อื่นๆ - เรายอมรับเฉพาะเพย์โหลด
application/jsonและจะส่งรหัสสถานะ406เพื่อระบุว่านี่ไม่ใช่รูปแบบเพย์โหลดที่ยอมรับได้
const books = req.body;
const writeBatch = firestore.batch();
for (const book of books) {
const doc = bookStore.doc(book.isbn);
writeBatch.set(doc, {
title: book.title,
author: book.author,
language: book.language,
pages: book.pages,
year: book.year,
updated: Firestore.Timestamp.now()
});
}
จากนั้นเราจะดึงข้อมูลเพย์โหลด JSON ผ่าน body ของคำขอได้ เรากำลังเตรียมการดำเนินการแบบกลุ่มใน Firestore เพื่อจัดเก็บหนังสือทั้งหมดพร้อมกัน เราจะวนซ้ำในอาร์เรย์ JSON ที่ประกอบด้วยรายละเอียดหนังสือ โดยจะวนซ้ำในช่อง isbn, title, author, language, pages และ year รหัส ISBN ของหนังสือจะทำหน้าที่เป็นคีย์หลักหรือตัวระบุ
try {
await writeBatch.commit();
console.log("Saved books in Firestore");
} catch (e) {
console.error("Error saving books:", e);
resp.status(400).send({error: "Error saving books"});
return;
};
resp.status(202).send({status: "OK"});
เมื่อข้อมูลส่วนใหญ่พร้อมแล้ว เราก็สามารถยืนยันการดำเนินการได้ หากการดำเนินการจัดเก็บข้อมูลไม่สำเร็จ เราจะแสดงรหัสสถานะ 400 เพื่อแจ้งให้ทราบว่าการดำเนินการไม่สำเร็จ ไม่เช่นนั้น เราจะส่งการตอบกลับ OK พร้อม202รหัสสถานะที่บ่งบอกว่าระบบยอมรับคำขอการบันทึกแบบกลุ่มแล้ว
การเรียกใช้และทดสอบฟังก์ชันการนำเข้า
ก่อนเรียกใช้โค้ด เราจะติดตั้งการอ้างอิงด้วยคำสั่งต่อไปนี้
$ npm install
หากต้องการเรียกใช้ฟังก์ชันในเครื่อง เราจะใช้คำสั่งสคริปต์ start ที่กำหนดไว้ใน package.json โดยใช้ Functions Framework
$ npm start > start > npx @google-cloud/functions-framework --target=parseBooks Serving function... Function: parseBooks URL: http://localhost:8080/
หากต้องการส่งคำขอ HTTP POST ไปยังฟังก์ชันในเครื่อง ให้เรียกใช้คำสั่งต่อไปนี้
$ curl -d "@../data/books.json" \
-H "Content-Type: application/json" \
http://localhost:8080/
เมื่อเปิดใช้คำสั่งนี้ คุณจะเห็นเอาต์พุตต่อไปนี้ ซึ่งยืนยันว่าฟังก์ชันทำงานในเครื่อง
{"status":"OK"}
นอกจากนี้ คุณยังไปที่ UI ของ 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 ที่ส่งออก) เป็นจุดแรกเข้า
หลังจากผ่านไปไม่เกิน 2 นาที ระบบจะติดตั้งใช้งานฟังก์ชันในระบบคลาวด์ ใน UI ของ Cloud Console คุณควรเห็นฟังก์ชันปรากฏขึ้น

ในเอาต์พุตการทำให้ใช้งานได้ คุณควรจะเห็น URL ของฟังก์ชันซึ่งเป็นไปตามรูปแบบการตั้งชื่อที่แน่นอน (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}) และแน่นอนว่าคุณยังดู URL ทริกเกอร์ HTTP นี้ได้ใน UI ของ Cloud Console ในแท็บทริกเกอร์

นอกจากนี้ คุณยังดึงข้อมูล URL ผ่านบรรทัดคำสั่งได้ด้วย gcloud
$ export BULK_IMPORT_URL=$(gcloud functions describe bulk-import \
--region=$REGION \
--format 'value(httpsTrigger.url)')
$ echo $BULK_IMPORT_URL
มาจัดเก็บไว้ในBULK_IMPORT_URLตัวแปรสภาพแวดล้อม เพื่อให้เรานำกลับมาใช้ใหม่ในการทดสอบฟังก์ชันที่ใช้งานจริงได้
การทดสอบฟังก์ชันที่ใช้งาน
เราจะทดสอบฟังก์ชันที่ใช้งานจริงด้วยคำสั่ง curl ที่คล้ายกันกับที่เราใช้ก่อนหน้านี้เพื่อทดสอบฟังก์ชันที่ทำงานในเครื่อง การเปลี่ยนแปลงเพียงอย่างเดียวคือ URL
$ curl -d "@../data/books.json" \
-H "Content-Type: application/json" \
$BULK_IMPORT_URL
หากสำเร็จ คำสั่งควรแสดงเอาต์พุตต่อไปนี้
{"status":"OK"}
ตอนนี้ฟังก์ชันการนำเข้าพร้อมใช้งานแล้ว และเราได้อัปโหลดข้อมูลตัวอย่างแล้ว จึงถึงเวลาที่เราจะพัฒนา REST API ที่แสดงชุดข้อมูลนี้
7. สัญญา REST API
แม้ว่าเราจะไม่ได้กำหนดสัญญา 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
}
GET /books
รับรายการหนังสือทั้งหมด ซึ่งอาจกรองตามผู้แต่งและ/หรือภาษา และแบ่งหน้าตามหน้าต่างที่มีผลลัพธ์ 10 รายการต่อครั้ง
เพย์โหลดของเนื้อหา: ไม่มี
พารามิเตอร์การค้นหา
author(ไม่บังคับ) - กรองรายการหนังสือตามผู้แต่งlanguage(ไม่บังคับ) — กรองรายการหนังสือตามภาษาpage(ไม่บังคับ ค่าเริ่มต้น = 0) — ระบุอันดับของหน้าผลการค้นหาที่จะแสดง
แสดงผล: อาร์เรย์ JSON ของออบเจ็กต์หนังสือ
รหัสสถานะ:
200— เมื่อคำขอเรียกข้อมูลรายการหนังสือสำเร็จ400— หากเกิดข้อผิดพลาด
POST /books และ POST /books/{isbn}
โพสต์เพย์โหลดหนังสือใหม่โดยมีisbnพารามิเตอร์เส้นทาง (ในกรณีนี้ไม่จำเป็นต้องมีรหัส isbn ในเพย์โหลดหนังสือ) หรือไม่มี (ในกรณีนี้ต้องมีรหัส isbn ในเพย์โหลดหนังสือ)
เพย์โหลดของเนื้อหา: ออบเจ็กต์หนังสือ
พารามิเตอร์การค้นหา: ไม่มี
การคืนสินค้า: ไม่มี
รหัสสถานะ:
201— เมื่อจัดเก็บหนังสือสำเร็จ406- หากรหัสisbnไม่ถูกต้อง400— หากเกิดข้อผิดพลาด
GET /books/{isbn}
ดึงข้อมูลหนังสือจากห้องสมุดโดยใช้รหัส isbn ที่ส่งเป็นพารามิเตอร์เส้นทาง
เพย์โหลดของเนื้อหา: ไม่มี
พารามิเตอร์การค้นหา: ไม่มี
แสดงผล: ออบเจ็กต์ JSON ของหนังสือ หรือออบเจ็กต์ข้อผิดพลาดหากไม่มีหนังสือ
รหัสสถานะ:
200— หากพบหนังสือในฐานข้อมูล400— หากเกิดข้อผิดพลาด404— หากไม่พบหนังสือ406— หากรหัสisbnไม่ถูกต้อง
PUT /books/{isbn}
อัปเดตหนังสือที่มีอยู่ซึ่งระบุโดย isbn ที่ส่งผ่านเป็นพารามิเตอร์เส้นทาง
เพย์โหลดของเนื้อหา: ออบเจ็กต์หนังสือ คุณส่งได้เฉพาะช่องที่ต้องอัปเดต ส่วนช่องอื่นๆ จะส่งหรือไม่ก็ได้
พารามิเตอร์การค้นหา: ไม่มี
ผลลัพธ์: หนังสือที่อัปเดตแล้ว
รหัสสถานะ:
200— เมื่ออัปเดตหนังสือสำเร็จ400— หากเกิดข้อผิดพลาด406— หากรหัสisbnไม่ถูกต้อง
DELETE /books/{isbn}
ลบหนังสือที่มีอยู่ซึ่งระบุโดย isbn ที่ส่งเป็นพารามิเตอร์เส้นทาง
เพย์โหลดของเนื้อหา: ไม่มี
พารามิเตอร์การค้นหา: ไม่มี
การคืนสินค้า: ไม่มี
รหัสสถานะ:
204— เมื่อลบหนังสือสำเร็จแล้ว400— หากเกิดข้อผิดพลาด
8. ทำให้ใช้งานได้และเปิดเผย REST 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 "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
แอปพลิเคชัน Web API ของเราขึ้นอยู่กับสิ่งต่อไปนี้
- โมดูล Firestore NPM เพื่อเข้าถึงข้อมูลหนังสือในฐานข้อมูล
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 เป็นเฟรมเวิร์กของเว็บเพื่อใช้ 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 และตอบกลับด้วยรหัสสถานะ 406 ในการตอบกลับ หากรหัส ISBN ไม่ถูกต้อง
GET /books
มาดูGET /booksอุปกรณ์ปลายทางทีละส่วนกัน
app.get('/books', async (req, res) => {
try {
var query = new Firestore().collection('books');
if (!!req.query.author) {
console.log(`Filtering by author: ${req.query.author}`);
query = query.where("author", "==", req.query.author);
}
if (!!req.query.language) {
console.log(`Filtering by language: ${req.query.language}`);
query = query.where("language", "==", req.query.language);
}
const page = parseInt(req.query.page) || 0;
// - - ✄ - - ✄ - - ✄ - - ✄ - - ✄ - -
} catch (e) {
console.error('Failed to fetch books', e);
res.status(400)
.send({error: `Impossible to fetch books: ${e.message}`});
}
});
เรากำลังเตรียมพร้อมที่จะค้นหาฐานข้อมูลโดยการเตรียมคำค้นหา การค้นหานี้จะขึ้นอยู่กับพารามิเตอร์การค้นหาที่ไม่บังคับ เพื่อกรองตามผู้แต่งและ/หรือตามภาษา นอกจากนี้ เรายังส่งคืนรายการหนังสือเป็นกลุ่มๆ ละ 10 เล่มด้วย
หากเกิดข้อผิดพลาดขณะดึงข้อมูลหนังสือ เราจะแสดงข้อผิดพลาดพร้อมรหัสสถานะ 400
มาซูมเข้าไปที่ส่วนที่ตัดของปลายทางนั้นกัน
const snapshot = await query
.orderBy('updated', 'desc')
.limit(PAGE_SIZE)
.offset(PAGE_SIZE * page)
.get();
const books = [];
if (snapshot.empty) {
console.log('No book found');
} else {
snapshot.forEach(doc => {
const {title, author, pages, year, language, ...otherFields} = doc.data();
const book = {isbn: doc.id, title, author, pages, year, language};
books.push(book);
});
}
ในส่วนก่อนหน้า เราได้กรองตาม author และ language แต่ในส่วนนี้ เราจะจัดเรียงรายการหนังสือตามลำดับวันที่อัปเดตล่าสุด (อัปเดตล่าสุดจะแสดงก่อน) นอกจากนี้ เราจะแบ่งหน้าผลลัพธ์ด้วยการกำหนดขีดจำกัด (จำนวนองค์ประกอบที่จะแสดง) และออฟเซ็ต (จุดเริ่มต้นที่จะแสดงหนังสือชุดถัดไป)
เราจะเรียกใช้การค้นหา รับสแนปชอตของข้อมูล และใส่ผลลัพธ์เหล่านั้นในอาร์เรย์ JavaScript ซึ่งจะแสดงผลเมื่อฟังก์ชันสิ้นสุด
มาดูแนวทางปฏิบัติที่ดีกันเพื่ออธิบายเกี่ยวกับปลายทางนี้ให้จบ นั่นคือการใช้ส่วนหัว Link เพื่อกำหนดลิงก์ URI ไปยังหน้าแรก ก่อนหน้า ถัดไป หรือหน้าสุดท้ายของข้อมูล (ในกรณีนี้ เราจะระบุเฉพาะหน้าก่อนหน้าและหน้าถัดไป)
var links = {};
if (page > 0) {
const prevQuery = querystring.stringify({...req.query, page: page - 1});
links.prev = `${req.path}${prevQuery != '' ? `?${prevQuery}` : ''}`;
}
if (snapshot.docs.length === PAGE_SIZE) {
const nextQuery = querystring.stringify({...req.query, page: page + 1});
links.next = `${req.path}${nextQuery != '' ? `?${nextQuery}` : ''}`;
}
if (Object.keys(links).length > 0) {
res.links(links);
}
res.status(200).send(books);
ตรรกะอาจดูซับซ้อนเล็กน้อยในตอนแรก แต่สิ่งที่เราทำคือการเพิ่มลิงก์ก่อนหน้าหากเราไม่ได้อยู่ที่หน้าแรกของข้อมูล และเราจะเพิ่มลิงก์ถัดไปหากหน้าข้อมูลเต็ม (เช่น มีหนังสือตามจำนวนสูงสุดที่กำหนดโดยค่าคงที่ PAGE_SIZE โดยสมมติว่ามีหน้าอื่นที่มีข้อมูลเพิ่มเติม) จากนั้นเราจะใช้resource#links()ฟังก์ชันของ Express เพื่อสร้างส่วนหัวที่ถูกต้องด้วยไวยากรณ์ที่ถูกต้อง
โปรดทราบว่าส่วนหัวของลิงก์จะมีลักษณะดังนี้
link: </books?page=1>; rel="prev", </books?page=3>; rel="next"
POST /booksและPOST /books/:isbn
ทั้ง 2 ปลายทางนี้มีไว้เพื่อสร้างหนังสือใหม่ โดยวิธีหนึ่งจะส่งรหัส ISBN ในเพย์โหลดของหนังสือ ส่วนอีกวิธีจะส่งเป็นพารามิเตอร์เส้นทาง ไม่ว่าจะใช้วิธีใด ทั้ง 2 วิธีจะเรียกใช้ฟังก์ชัน createBook() ของเรา
async function createBook(isbn, req, res) {
const parsedIsbn = isbnOK(isbn, res);
if (!parsedIsbn) return;
const {title, author, pages, year, language} = req.body;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.set({
title, author, pages, year, language,
updated: Firestore.Timestamp.now()
});
console.log(`Saved book ${parsedIsbn.isbn13}`);
res.status(201)
.location(`/books/${parsedIsbn.isbn13}`)
.send({status: `Book ${parsedIsbn.isbn13} created`});
} catch (e) {
console.error(`Failed to save book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to create book ${parsedIsbn.isbn13}: ${e.message}`});
}
}
เราจะตรวจสอบว่ารหัส isbn ถูกต้องหรือไม่ หากไม่ถูกต้อง ฟังก์ชันจะส่งคืน (และตั้งรหัสสถานะ 406) เราจะดึงข้อมูลฟิลด์หนังสือจากเพย์โหลดที่ส่งในเนื้อหาของคำขอ จากนั้นเราจะจัดเก็บรายละเอียดหนังสือใน Firestore โดยจะแสดง 201 เมื่อสำเร็จ และ 400 เมื่อไม่สำเร็จ
เมื่อส่งคืนสำเร็จ เราจะตั้งค่าส่วนหัวของตำแหน่งด้วย เพื่อให้คำแนะนำแก่ไคลเอ็นต์ของ 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}`});
}
});
เราจะตรวจสอบว่า ISBN ถูกต้องหรือไม่เสมอ เราจะทำการค้นหาใน Firestore เพื่อดึงข้อมูลหนังสือ พร็อพเพอร์ตี้ snapshot.exists มีประโยชน์ในการดูว่าพบหนังสือหรือไม่ ไม่เช่นนั้น เราจะส่งข้อผิดพลาดและรหัสสถานะ 404 ไม่พบกลับไป เราจะดึงข้อมูลหนังสือและสร้างออบเจ็กต์ JSON ที่แสดงถึงหนังสือเพื่อส่งกลับ
PUT /books/:isbn
เราใช้วิธี PUT เพื่ออัปเดตหนังสือที่มีอยู่
app.put('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.set({
...req.body,
updated: Firestore.Timestamp.now()
}, {merge: true});
console.log(`Updated book ${parsedIsbn.isbn13}`);
res.status(201)
.location(`/books/${parsedIsbn.isbn13}`)
.send({status: `Book ${parsedIsbn.isbn13} updated`});
} catch (e) {
console.error(`Failed to update book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to update book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
เราจะอัปเดตupdatedฟิลด์วันที่/เวลาเพื่อจดจำเวลาที่เราอัปเดตระเบียนนั้นล่าสุด เราใช้กลยุทธ์ {merge:true} ซึ่งจะแทนที่ช่องที่มีอยู่ด้วยค่าใหม่ (มิฉะนั้น ระบบจะนำช่องทั้งหมดออก และจะบันทึกเฉพาะช่องใหม่ในเพย์โหลดเท่านั้น ซึ่งจะเป็นการลบช่องที่มีอยู่จากการอัปเดตครั้งก่อนหรือการสร้างครั้งแรก)
นอกจากนี้ เรายังตั้งค่าส่วนหัว Location ให้ชี้ไปยัง URI ของหนังสือด้วย
DELETE /books/:isbn
การลบหนังสือเป็นเรื่องง่าย เราเพียงเรียกใช้เมธอด delete() ในการอ้างอิงเอกสาร เราจะแสดงรหัสสถานะ 204 เนื่องจากเราไม่ได้แสดงเนื้อหาใดๆ
app.delete('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.delete();
console.log(`Book ${parsedIsbn.isbn13} was deleted`);
res.status(204).end();
} catch (e) {
console.error(`Failed to delete book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to delete book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
เริ่มเซิร์ฟเวอร์ Express / 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
ไม่ว่าเราจะเรียกใช้โค้ด REST API อย่างไร (โดยตรงผ่าน Node หรือผ่านอิมเมจคอนเทนเนอร์ Docker) ตอนนี้เราก็เรียกใช้การค้นหา 2-3 รายการกับโค้ดนั้นได้แล้ว
- สร้างหนังสือใหม่ (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
- โหลดหน้า 4 ของหนังสือ
$ curl http://localhost:8080/books?page=3
นอกจากนี้ เรายังรวมพารามิเตอร์การค้นหา author, language และ books เพื่อปรับแต่งการค้นหาได้ด้วย
การสร้างและติดตั้งใช้งาน REST API ที่อยู่ในคอนเทนเนอร์
เนื่องจากเรายินดีที่ REST API ทำงานได้ตามแผน จึงเป็นเวลาที่เหมาะสมที่จะติดตั้งใช้งานในระบบคลาวด์บน Cloud Run
โดยเราจะดำเนินการใน 2 ขั้นตอน ดังนี้
- ก่อนอื่น ให้สร้างอิมเมจคอนเทนเนอร์ด้วย Cloud Build โดยใช้คำสั่งต่อไปนี้
$ gcloud builds submit \
--tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
- จากนั้นให้ติดตั้งใช้งานบริการด้วยคำสั่งที่ 2 ดังนี้
$ gcloud run deploy run-crud \
--image gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api \
--allow-unauthenticated \
--region=${REGION} \
--platform=managed
คำสั่งแรกจะทำให้ Cloud Build สร้างอิมเมจคอนเทนเนอร์และโฮสต์ใน Container Registry คำสั่งถัดไปจะทําการติดตั้งใช้งานอิมเมจคอนเทนเนอร์จากรีจิสทรี และติดตั้งใช้งานในภูมิภาคของระบบคลาวด์
เราสามารถตรวจสอบอีกครั้งใน UI ของ 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 ของ REST API ของ Cloud Run ในส่วนถัดไป เนื่องจากโค้ดส่วนหน้าของ App Engine จะโต้ตอบกับ API
9. โฮสต์เว็บแอปเพื่อเรียกดูคลัง
ส่วนสุดท้ายของปริศนาที่จะเพิ่มความน่าสนใจให้กับโปรเจ็กต์นี้คือการจัดหาฟรอนท์เอนด์ของเว็บที่จะโต้ตอบกับ REST API ของเรา ด้วยเหตุนี้ เราจะใช้ Google App Engine กับโค้ด JavaScript ของไคลเอ็นต์บางส่วนที่จะเรียก API ผ่านคำขอ AJAX (โดยใช้ API Fetch ฝั่งไคลเอ็นต์)
แม้ว่าแอปพลิเคชันของเราจะได้รับการติดตั้งใช้งานในรันไทม์ของ App Engine สำหรับ Node.JS แต่ส่วนใหญ่ก็สร้างขึ้นจากทรัพยากรแบบคงที่ ไม่มีโค้ดแบ็กเอนด์มากนัก เนื่องจากผู้ใช้ส่วนใหญ่จะโต้ตอบในเบราว์เซอร์ผ่าน JavaScript ฝั่งไคลเอ็นต์ เราจะไม่ใช้เฟรมเวิร์ก JavaScript ที่ซับซ้อนในส่วนหน้า แต่จะใช้ JavaScript "ธรรมดา" ร่วมกับ Web Components บางรายการสำหรับ UI โดยใช้ไลบรารี Web Components ของ Shoelace
- กล่องเลือกเพื่อเลือกภาษาของหนังสือ

- คอมโพเนนต์การ์ดเพื่อแสดงรายละเอียดเกี่ยวกับหนังสือเล่มหนึ่งๆ (รวมถึงบาร์โค้ดที่แสดง ISBN ของหนังสือโดยใช้ไลบรารี JsBarcode)

- และปุ่มเพื่อโหลดหนังสือเพิ่มเติมจากฐานข้อมูล

เมื่อรวมคอมโพเนนต์ภาพทั้งหมดเข้าด้วยกัน เว็บเพจที่ได้สำหรับการเรียกดูคลังจะมีลักษณะดังนี้

app.yaml ไฟล์การกำหนดค่า
มาเริ่มเจาะลึกฐานของโค้ดของแอปพลิเคชัน App Engine นี้กันโดยดูที่app.yamlไฟล์การกำหนดค่า ไฟล์นี้เป็นไฟล์เฉพาะของ App Engine และช่วยให้กำหนดค่าสิ่งต่างๆ ได้ เช่น ตัวแปรสภาพแวดล้อม "ตัวแฮนเดิล" ต่างๆ ของแอปพลิเคชัน หรือการระบุว่าทรัพยากรบางอย่างเป็นเนื้อหาสแตติกซึ่งจะแสดงโดย CDN ในตัวของ App Engine
runtime: nodejs14
env_variables:
RUN_CRUD_SERVICE_URL: CHANGE_ME
handlers:
- url: /js
static_dir: public/js
- url: /css
static_dir: public/css
- url: /img
static_dir: public/img
- url: /(.+\.html)
static_files: public/html/\1
upload: public/(.+\.html)
- url: /
static_files: public/html/index.html
upload: public/html/index\.html
- url: /.*
secure: always
script: auto
เราระบุว่าแอปพลิเคชันของเราเป็นแอปพลิเคชัน Node.JS และเราต้องการใช้เวอร์ชัน 14
จากนั้นเราจะกำหนดตัวแปรสภาพแวดล้อมที่ชี้ไปยัง URL ของบริการ Cloud Run เราจะต้องอัปเดตตัวยึดตำแหน่ง CHANGE_ME ด้วย URL ที่ถูกต้อง (ดูวิธีเปลี่ยนได้ที่ด้านล่าง)
หลังจากนั้น เราจะกำหนดตัวแฮนเดิลต่างๆ 3 รายการแรกชี้ไปยังตำแหน่งโค้ดฝั่งไคลเอ็นต์ของ HTML, CSS และ JavaScript ภายใต้โฟลเดอร์ public/ และโฟลเดอร์ย่อย ส่วนรายการที่ 4 ระบุว่า URL รูทของแอปพลิเคชัน App Engine ควรชี้ไปที่หน้า index.html ด้วยวิธีนี้ เราจะไม่เห็นคำต่อท้าย index.html ใน URL เมื่อเข้าถึงรูทของเว็บไซต์ และสุดท้ายคือค่าเริ่มต้นที่จะกำหนดเส้นทาง URL อื่นๆ ทั้งหมด (/.*) ไปยังแอปพลิเคชัน Node.JS (เช่น ส่วน "ไดนามิก" ของแอปพลิเคชัน ซึ่งตรงกันข้ามกับชิ้นงานแบบคงที่เราได้อธิบายไว้)
มาอัปเดต URL ของ Web API ของบริการ Cloud Run กันเลย
ในไดเรกทอรี appengine-frontend/ ให้เรียกใช้คำสั่งต่อไปนี้เพื่ออัปเดตตัวแปรสภาพแวดล้อมที่ชี้ไปยัง URL ของ REST API ที่อิงตาม Cloud Run
$ sed -i -e "s|CHANGE_ME|${RUN_CRUD_SERVICE_URL}|" app.yaml
หรือเปลี่ยนCHANGE_MEสตริงในapp.yamlด้วยตนเองโดยใช้ URL ที่ถูกต้อง ดังนี้
env_variables:
RUN_CRUD_SERVICE_URL: CHANGE_ME
package.json ไฟล์ Node.JS
{
"name": "appengine-frontend",
"description": "Web frontend",
"license": "Apache-2.0",
"main": "index.js",
"engines": {
"node": "^14.0.0"
},
"dependencies": {
"express": "^4.17.1",
"isbn3": "^1.1.10"
},
"devDependencies": {
"nodemon": "^2.0.7"
},
"scripts": {
"start": "node index.js",
"dev": "nodemon --watch server --inspect index.js"
}
}
เราขอย้ำอีกครั้งว่าเราต้องการเรียกใช้แอปพลิเคชันนี้โดยใช้ Node.JS 14 เราใช้เฟรมเวิร์ก Express รวมถึงisbn3โมดูล NPM สำหรับการตรวจสอบรหัส ISBN ของหนังสือ
ในทรัพยากร Dependency ของการพัฒนา เราจะใช้โมดูล 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
มาดูเส้นทาง 2 เส้นทางที่เรากำหนดไว้กัน
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 แทน
ปลายทางที่ 2 ที่เรากำหนด /webapi จะแสดง URL ของ REST API ของ Cloud RUN ด้วยวิธีนี้ โค้ด JavaScript ฝั่งไคลเอ็นต์จะรู้ว่าจะเรียกใช้ที่ใดเพื่อรับรายชื่อหนังสือ
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Book library web frontend: listening on port ${port}`);
console.log(`Node ${process.version}`);
console.log(`Web API endpoint ${process.env.RUN_CRUD_SERVICE_URL}`);
});
สุดท้ายนี้ เราจะเรียกใช้เว็บแอป Express และรับฟังบนพอร์ต 8080 โดยค่าเริ่มต้น
index.html หน้า
เราจะไม่ดูทุกบรรทัดของหน้า HTML ที่ยาวนี้ แต่เราจะไฮไลต์บรรทัดสำคัญบางบรรทัดแทน
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/themes/base.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/shoelace.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/barcodes/JsBarcode.ean-upc.min.js"></script>
<script src="/js/app.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css">
2 บรรทัดแรกจะนำเข้าไลบรารีคอมโพเนนต์เว็บของ 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 ฝั่งไคลเอ็นต์ที่โต้ตอบกับ REST API ของเรา
โค้ด JavaScript ฝั่งไคลเอ็นต์ app.js
เราเริ่มต้นด้วย 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 ของ REST API โดยใช้โค้ดโหนด App Engine ซึ่งจะแสดงตัวแปรสภาพแวดล้อมที่เราตั้งค่าไว้ตอนแรกใน app.yaml ตัวแปรสภาพแวดล้อมและปลายทาง /webapi ที่เรียกจากโค้ดฝั่งไคลเอ็นต์ JavaScript ทำให้เราไม่ต้องฮาร์ดโค้ด URL ของ REST API ในโค้ดส่วนหน้า
นอกจากนี้ เรายังกำหนดตัวแปร page และ language ซึ่งเราจะใช้เพื่อติดตามการแบ่งหน้าและการกรองภาษา
const moreButton = document.getElementById('more-button');
moreButton.addEventListener('sl-focus', event => {
console.log('Button clicked');
moreButton.blur();
appendMoreBooks(server, page++, language);
});
เราเพิ่มตัวแฮนเดิลเหตุการณ์ในปุ่มเพื่อโหลดหนังสือ เมื่อคลิก ระบบจะเรียกใช้ฟังก์ชัน appendMoreBooks()
const langSelect = document.getElementById('language-select');
langSelect.addEventListener('sl-change', event => {
page = 0;
language = event.srcElement.value;
document.getElementById('library').replaceChildren();
console.log(`Language selected: "${language}"`);
appendMoreBooks(server, page++, language);
});
เช่นเดียวกับช่องตัวเลือก เราจะเพิ่มตัวแฮนเดิลเหตุการณ์เพื่อให้ได้รับการแจ้งเตือนเมื่อมีการเปลี่ยนแปลงในการเลือกภาษา และเช่นเดียวกับปุ่ม เรายังเรียกใช้ฟังก์ชัน appendMoreBooks() โดยส่ง URL ของ REST API, หน้าปัจจุบัน และการเลือกภาษาด้วย
มาดูฟังก์ชันที่ดึงและต่อท้ายหนังสือกัน
async function appendMoreBooks(server, page, language) {
const searchUrl = new URL(server);
if (!!page) searchUrl.searchParams.append('page', page);
if (!!language) searchUrl.searchParams.append('language', language);
const response = await fetch(searchUrl.href);
const books = await response.json();
...
}
ด้านบน เรากำลังสร้าง URL ที่แน่นอนเพื่อใช้เรียก REST API โดยปกติแล้วเราจะระบุพารามิเตอร์การค้นหาได้ 3 รายการ แต่ใน UI นี้ เราจะระบุเพียง 2 รายการ ดังนี้
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';
}
เราจะแสดงหรือซ่อนปุ่ม [More books...] ขึ้นอยู่กับว่ามีส่วนหัว Link ในการตอบกลับหรือไม่ เนื่องจากส่วนหัว Link เป็นคำแนะนำที่บอกเราว่ายังมีหนังสือที่ต้องโหลดอีกหรือไม่ (จะมี URL next ในส่วนหัว Link)
const library = document.getElementById('library');
const template = document.getElementById('book-card');
for (let book of books) {
const bookCard = template.content.cloneNode(true);
bookCard.querySelector('slot[name=title]').innerText = book.title;
bookCard.querySelector('slot[name=language]').innerText = book.language;
bookCard.querySelector('slot[name=author]').innerText = book.author;
bookCard.querySelector('slot[name=year]').innerText = book.year;
bookCard.querySelector('slot[name=pages]').innerText = book.pages;
const img = document.createElement('img');
img.setAttribute('id', book.isbn);
img.setAttribute('class', 'img-barcode-' + book.isbn)
bookCard.querySelector('slot[name=barcode]').appendChild(img);
library.appendChild(bookCard);
...
}
}
ในส่วนฟังก์ชันด้านบน สำหรับหนังสือแต่ละเล่มที่ REST API แสดงผล เราจะโคลนเทมเพลตด้วยคอมโพเนนต์เว็บบางอย่างที่แสดงถึงหนังสือ และจะป้อนข้อมูลช่องของเทมเพลตด้วยรายละเอียดของหนังสือ
JsBarcode('.img-barcode-' + book.isbn).EAN13(book.isbn, {fontSize: 18, textMargin: 0, height: 60}).render();
เราใช้ไลบรารี JsBarcode เพื่อสร้างบาร์โค้ดที่ดูดีเหมือนกับที่อยู่บนปกหลังของหนังสือจริง เพื่อให้รหัส ISBN ดูดีขึ้นเล็กน้อย
เรียกใช้และทดสอบแอปพลิเคชันในเครื่อง
ตอนนี้โค้ดก็เพียงพอแล้ว ได้เวลาดูการทำงานของแอปพลิเคชัน ก่อนอื่น เราจะดำเนินการในเครื่องภายใน Cloud Shell ก่อนที่จะทำให้ใช้งานได้จริง
เราติดตั้งโมดูล NPM ที่แอปพลิเคชันของเราต้องการด้วยคำสั่งต่อไปนี้
$ npm install
และเราจะเรียกใช้แอปด้วยวิธีปกติอย่างใดอย่างหนึ่งต่อไปนี้
$ npm start
หรือใช้การโหลดการเปลี่ยนแปลงซ้ำโดยอัตโนมัติด้วย nodemon โดยมีสิ่งต่อไปนี้
$ npm run dev
แอปพลิเคชันทำงานในเครื่อง และเราเข้าถึงได้จากเบราว์เซอร์ที่ http://localhost:8080
การทําให้แอปพลิเคชัน App Engine ใช้งานได้
เมื่อมั่นใจว่าแอปพลิเคชันทำงานได้ดีในเครื่องแล้ว ก็ถึงเวลาที่จะทําให้ใช้งานได้ใน App Engine
หากต้องการทำให้แอปพลิเคชันใช้งานได้ ให้เรียกใช้คำสั่งต่อไปนี้
$ gcloud app deploy -q
หลังจากผ่านไปประมาณ 1 นาที ระบบควรจะติดตั้งใช้งานแอปพลิเคชัน
แอปพลิเคชันจะพร้อมใช้งานที่ URL ในรูปแบบ https://${GOOGLE_CLOUD_PROJECT}.appspot.com
การสำรวจ UI ของเว็บแอปพลิเคชัน App Engine
ตอนนี้คุณทำสิ่งต่อไปนี้ได้แล้ว
- คลิกปุ่ม
[More books...]เพื่อโหลดหนังสือเพิ่มเติม - เลือกภาษาที่ต้องการเพื่อดูเฉพาะหนังสือในภาษานั้น
- คุณล้างการเลือกได้โดยใช้กากบาทเล็กๆ ในช่องเลือก เพื่อกลับไปที่รายการหนังสือทั้งหมด
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 ทั่วไปให้กับฟังก์ชันการนำเข้าข้อมูลและคอนเทนเนอร์ REST API เพื่อเพิ่มฟีเจอร์ต่างๆ เช่น การจัดการคีย์ API เพื่อเข้าถึง API หรือกำหนดขีดจำกัดอัตราสำหรับผู้ใช้ API
- ทำให้โมดูลโหนด Swagger-UI ใช้งานได้ในแอปพลิเคชัน App Engine เพื่อจัดทำเอกสารและเสนอสนามทดสอบสำหรับ REST API
- ในส่วนฟรอนท์เอนด์ นอกเหนือจากความสามารถในการเรียกดูที่มีอยู่แล้ว ให้เพิ่มหน้าจอพิเศษเพื่อแก้ไขข้อมูลและสร้างรายการหนังสือใหม่ นอกจากนี้ เนื่องจากเราใช้ฐานข้อมูล Cloud Firestore ให้ใช้ประโยชน์จากฟีเจอร์เรียลไทม์เพื่ออัปเดตข้อมูลหนังสือที่แสดงเมื่อมีการเปลี่ยนแปลง