1. ภาพรวม
เป้าหมายของ Codelab นี้คือการรับประสบการณ์ในการใช้งานแบบ "Serverless" บริการจาก Google Cloud Platform:
- ฟังก์ชันระบบคลาวด์ — เพื่อปรับใช้หน่วยตรรกะทางธุรกิจขนาดเล็กให้เป็นฟังก์ชัน ซึ่งตอบสนองต่อเหตุการณ์ต่างๆ (ข้อความ Pub/Sub, ไฟล์ใหม่ใน Cloud Storage, คำขอ HTTP และอื่นๆ)
- App Engine — เพื่อปรับใช้และให้บริการเว็บแอป, API บนเว็บ, แบ็กเอนด์อุปกรณ์เคลื่อนที่, เนื้อหาแบบคงที่ พร้อมความสามารถในการเพิ่มและลดอย่างรวดเร็ว
- Cloud Run — เพื่อทำให้ใช้งานได้และปรับขนาดคอนเทนเนอร์ซึ่งอาจมีภาษา รันไทม์ หรือไลบรารีใดก็ได้
และเพื่อสำรวจวิธีใช้ประโยชน์จากบริการแบบ Serverless เหล่านี้เพื่อทำให้ Web และ REST API ใช้งานได้และปรับขนาด 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 ของระบบคลาวด์ การใช้งาน Codelab นี้จะไม่มีค่าใช้จ่ายใดๆ หากมี หากต้องการปิดทรัพยากรเพื่อหลีกเลี่ยงการเรียกเก็บเงินที่นอกเหนือจากบทแนะนำนี้ คุณสามารถลบทรัพยากรที่คุณสร้างหรือลบโปรเจ็กต์ได้ ผู้ใช้ Google Cloud ใหม่มีสิทธิ์เข้าร่วมโปรแกรมช่วงทดลองใช้ฟรี$300 USD
เริ่มต้น Cloud Shell
แม้ว่าคุณจะดำเนินการ Google Cloud จากระยะไกลได้จากแล็ปท็อป แต่คุณจะใช้ Google Cloud Shell ซึ่งเป็นสภาพแวดล้อมแบบบรรทัดคำสั่งที่ทำงานในระบบคลาวด์ใน Codelab นี้
จากคอนโซล Google Cloud ให้คลิกไอคอน Cloud Shell ในแถบเครื่องมือด้านขวาบน ดังนี้
การจัดสรรและเชื่อมต่อกับสภาพแวดล้อมนี้ควรใช้เวลาเพียงครู่เดียว เมื่อเสร็จแล้ว คุณจะเห็นข้อมูลต่อไปนี้
เครื่องเสมือนนี้เต็มไปด้วยเครื่องมือการพัฒนาทั้งหมดที่คุณต้องการ โดยมีไดเรกทอรีหลักขนาด 5 GB ที่ใช้งานได้ต่อเนื่องและทำงานบน Google Cloud ซึ่งช่วยเพิ่มประสิทธิภาพของเครือข่ายและการตรวจสอบสิทธิ์ได้อย่างมาก งานทั้งหมดใน Codelab นี้ทำได้ในเบราว์เซอร์ คุณไม่จำเป็นต้องติดตั้งอะไรเลย
3. เตรียมสภาพแวดล้อมและเปิดใช้ Cloud API
เพื่อที่จะใช้บริการต่างๆ ที่เราต้องใช้ตลอดโครงการนี้ เราจะเปิดใช้ API 2-3 รายการ โดยการเรียกใช้คำสั่งต่อไปนี้ใน 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 มาใช้ใน Codelab นี้ เราจะต้องจัดเรียงและกรองข้อมูล ด้วยเหตุนี้ เราจะสร้างดัชนี 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
ที่มีรายชื่อหนังสือกว่าร้อยเล่มน่าจะควรค่าแก่การอ่าน เอกสาร JSON นี้เป็นอาร์เรย์ที่มีออบเจ็กต์ JSON มาดูรูปร่างข้อมูลที่เราจะนำเข้าผ่าน Cloud Function กัน
[
{
"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/firestore
NPM เพื่อเข้าถึงฐานข้อมูลและจัดเก็บข้อมูลหนังสือ รันไทม์ของ Cloud Functions ในระบบขั้นสูงยังมีเฟรมเวิร์กเว็บ Express ด้วย เราจึงไม่ต้องประกาศว่าเป็นทรัพยากร Dependency
ในทรัพยากร Dependency ของการพัฒนา เราประกาศเฟรมเวิร์กฟังก์ชัน (@google-cloud/functions-framework
) ซึ่งเป็นเฟรมเวิร์กรันไทม์ที่ใช้ในการเรียกใช้ฟังก์ชัน ซึ่งเป็นเฟรมเวิร์กโอเพนซอร์สที่คุณใช้ภายในเครื่องได้ (ในกรณีของเราคือภายใน Cloud Shell) เพื่อเรียกใช้ฟังก์ชันโดยไม่ต้องทำให้ใช้งานได้ทุกครั้งที่ทำการเปลี่ยนแปลง ซึ่งจะช่วยปรับปรุงลูปความคิดเห็นสำหรับการพัฒนา
หากต้องการติดตั้งทรัพยากร Dependency ให้ใช้คำสั่ง install
$ npm install
สคริปต์ start
จะใช้เฟรมเวิร์กฟังก์ชันเพื่อมอบคำสั่งที่ใช้เพื่อเรียกใช้ฟังก์ชันในเครื่องตามคำสั่งต่อไปนี้
$ npm start
คุณสามารถใช้ Curl หรือตัวอย่างเว็บ Cloud Shell สำหรับคำขอ HTTP GET เพื่อโต้ตอบกับฟังก์ชัน
คราวนี้เรามาดูไฟล์ index.js
ที่มีตรรกะของฟังก์ชันนำเข้าข้อมูลหนังสือ ดังนี้
const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');
เราจะสร้างอินสแตนซ์ของโมดูล Firestore และชี้ไปที่คอลเล็กชันหนังสือ (คล้ายกับตารางในฐานข้อมูลเชิงสัมพันธ์)
functions.http('parseBooks', async (req, resp) => {
if (req.method !== "POST") {
resp.status(405).send({error: "Only method POST allowed"});
return;
}
if (req.headers['content-type'] !== "application/json") {
resp.status(406).send({error: "Only application/json accepted"});
return;
}
...
})
เรากำลังส่งออกฟังก์ชัน JavaScript parseBooks
นี่คือฟังก์ชันที่เราจะประกาศเมื่อเราทำให้ใช้งานได้ในภายหลัง
วิธีการ 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
เพื่อแจ้งว่าการดำเนินการนี้ล้มเหลว ไม่เช่นนั้นเราอาจแสดงผลการตอบกลับ "ตกลง" ซึ่งมีรหัสสถานะ 202
ระบุว่ายอมรับคำขอบันทึกจำนวนมากแล้ว
การเรียกใช้และทดสอบฟังก์ชันการนำเข้า
ก่อนเรียกใช้โค้ด เราจะติดตั้ง Dependencies ที่มี:
$ npm install
หากต้องการเรียกใช้ฟังก์ชันภายในเครื่อง การใช้เฟรมเวิร์กฟังก์ชันจะใช้คำสั่งสคริปต์ start
ที่เรากำหนดไว้ใน package.json
ดังนี้
$ npm start > start > npx @google-cloud/functions-framework --target=parseBooks Serving function... Function: parseBooks URL: http://localhost:8080/
หากต้องการส่งคำขอ HTTP POST
ไปยังฟังก์ชันในเครื่อง คุณเรียกใช้สิ่งต่อไปนี้ได้
$ curl -d "@../data/books.json" \ -H "Content-Type: application/json" \ http://localhost:8080/
เมื่อเรียกใช้คำสั่งนี้ คุณจะเห็นเอาต์พุตต่อไปนี้เพื่อยืนยันว่าฟังก์ชันกำลังทำงานในเครื่อง
{"status":"OK"}
นอกจากนี้ คุณยังไปที่ 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-3 นาที ระบบจะทำให้ฟังก์ชันใช้งานได้ในระบบคลาวด์ คุณควรเห็นฟังก์ชันใน 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
}
ดาวน์โหลด /หนังสือ
ดูรายชื่อหนังสือทั้งหมด ซึ่งอาจกรองตามผู้แต่งและ/หรือภาษา และแบ่งหน้าตามผลการค้นหาครั้งละ 10 เล่ม
เพย์โหลดเนื้อหา: ไม่มี
พารามิเตอร์การค้นหา:
author
(ไม่บังคับ) — กรองรายการหนังสือตามผู้แต่งlanguage
(ไม่บังคับ) — กรองรายการหนังสือตามภาษาpage
(ไม่บังคับ ค่าเริ่มต้น = 0) — แสดงถึงอันดับของหน้าผลลัพธ์ที่จะแสดงผล
ผลลัพธ์: อาร์เรย์ JSON ของออบเจ็กต์หนังสือ
รหัสสถานะ:
200
— เมื่อคำขอดึงข้อมูลรายชื่อหนังสือสำเร็จ400
— หากเกิดข้อผิดพลาด
โพสต์ /books และ POST /books/{isbn}
โพสต์เพย์โหลดหนังสือใหม่ ไม่ว่าจะมีพารามิเตอร์เส้นทาง isbn
(ในกรณีที่ไม่จำเป็นต้องใช้รหัส isbn
ในเพย์โหลดหนังสือ) หรือไม่มี (ในกรณีนี้ต้องมีรหัส isbn
อยู่ในเพย์โหลดของหนังสือ)
เพย์โหลดเนื้อหา: ออบเจ็กต์หนังสือ
พารามิเตอร์การค้นหา: ไม่มี
ส่งคืน: ไม่มี
รหัสสถานะ:
201
— เมื่อจัดเก็บหนังสือเสร็จเรียบร้อยแล้ว406
— หากรหัสisbn
ไม่ถูกต้อง400
— หากเกิดข้อผิดพลาด
ดาวน์โหลด /books/{isbn}
เรียกดูหนังสือจากห้องสมุด ซึ่งระบุด้วยรหัส isbn
ของหนังสือเล่มนั้นๆ และส่งผ่านเป็นพารามิเตอร์เส้นทาง
เพย์โหลดเนื้อหา: ไม่มี
พารามิเตอร์การค้นหา: ไม่มี
การแสดงผล: ออบเจ็กต์ JSON ของหนังสือ หรือออบเจ็กต์แสดงข้อผิดพลาดหากไม่มีหนังสือ
รหัสสถานะ:
200
— หากพบหนังสือในฐานข้อมูล400
— หากเกิดข้อผิดพลาด404
— หากไม่พบหนังสือ406
— หากรหัสisbn
ไม่ถูกต้อง
PUT /หนังสือ/{isbn}
อัปเดตหนังสือที่มีอยู่ซึ่งระบุโดย isbn
ที่ส่งผ่านเป็นพารามิเตอร์เส้นทาง
เพย์โหลดเนื้อหา: ออบเจ็กต์หนังสือ ระบบสามารถส่งต่อเฉพาะช่องที่ต้องทำการอัปเดต ส่วนช่องอื่นไม่จำเป็นต้องกรอกข้อมูล
พารามิเตอร์การค้นหา: ไม่มี
การส่งคืน: หนังสือที่อัปเดตแล้ว
รหัสสถานะ:
200
— เมื่ออัปเดตหนังสือเรียบร้อยแล้ว400
— หากเกิดข้อผิดพลาด406
— หากรหัสisbn
ไม่ถูกต้อง
ลบ /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" ]
เราใช้อิมเมจ "Slim" ของ Node.JS 20 เรากำลังดำเนินการในไดเรกทอรี /usr/src/app
เรากำลังคัดลอกไฟล์ package.json
(ตามรายละเอียดด้านล่าง) ที่กำหนดทรัพยากร Dependency ของเรา รวมถึงสิ่งอื่นๆ เราติดตั้งทรัพยากร Dependency ด้วย 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 - ซึ่งเป็นเฟรมเวิร์กเว็บสำหรับการออกแบบ 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 ในเพย์โหลดหนังสือ ขณะที่อีกฉบับส่งผ่านเป็นพารามิเตอร์เส้นทาง ไม่ว่าจะใช้วิธีใด ให้เรียกใช้ฟังก์ชัน 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 / โหนด
สุดท้ายแต่ไม่ท้ายสุด เราเริ่มต้นเซิร์ฟเวอร์โดยรออ่านบนพอร์ต 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}`);
});
การเรียกใช้แอปพลิเคชันในเครื่อง
หากต้องการเรียกใช้แอปพลิเคชันในเครื่อง ก่อนอื่นเราจะติดตั้งทรัพยากร Dependency ด้วย
$ 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 (ผ่านโหนดหรือผ่านอิมเมจคอนเทนเนอร์ 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 บน 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 ของ Cloud Run REST API ในหัวข้อถัดไป เนื่องจากโค้ดฟรอนท์เอนด์ของ App Engine จะโต้ตอบกับ API ดังกล่าว
9. โฮสต์เว็บแอปเพื่อเรียกดูไลบรารี
ปริศนาส่วนสุดท้ายที่จะเพิ่มความแวววับให้กับโปรเจ็กต์นี้ คือการมอบฟรอนท์เอนด์ของเว็บที่จะโต้ตอบกับ REST API ของเรา สำหรับวัตถุประสงค์ดังกล่าว เราจะใช้ Google App Engine กับโค้ด JavaScript ของไคลเอ็นต์ซึ่งจะเรียกใช้ API ผ่านคำขอ AJAX (โดยใช้ API ดึงข้อมูล ฝั่งไคลเอ็นต์)
แม้ว่าแอปพลิเคชันของเราจะใช้งานในรันไทม์ของ Node.JS App Engine แต่แอปพลิเคชันส่วนใหญ่ก็สร้างขึ้นจากทรัพยากรแบบคงที่ ไม่มีโค้ดแบ็กเอนด์มาก เนื่องจากการโต้ตอบส่วนใหญ่ของผู้ใช้จะอยู่ในเบราว์เซอร์ผ่าน JavaScript ฝั่งไคลเอ็นต์ เราจะไม่ใช้เฟรมเวิร์ก JavaScript ฟรอนท์เอนด์ราคาพิเศษ เราจะใช้ JavaScript "vanilla" บางส่วนกับคอมโพเนนต์เว็บบางรายการสำหรับ UI ที่ใช้ไลบรารีคอมโพเนนต์เว็บเชือกผูก ดังนี้
- กล่องสำหรับเลือกเพื่อเลือกภาษาของหนังสือ:
- คอมโพเนนต์การ์ดที่จะแสดงรายละเอียดเกี่ยวกับหนังสือเล่มใดเล่มหนึ่ง (รวมถึงบาร์โค้ดที่ใช้แสดง 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 สุดท้ายคือ URL เริ่มต้นที่จะกำหนดเส้นทาง URL อื่นๆ ทั้งหมด (/.*
) ไปยังแอปพลิเคชัน Node.JS (เช่น ส่วน "dynamic" ของแอปพลิเคชัน ซึ่งแตกต่างจากเนื้อหาแบบคงที่ที่เราได้อธิบายไว้)
มาอัปเดต URL ของ Web API ของบริการ Cloud Run กันเลย
ในไดเรกทอรี appengine-frontend/
ให้เรียกใช้คำสั่งต่อไปนี้เพื่ออัปเดตตัวแปรสภาพแวดล้อมซึ่งชี้ไปที่ URL ของ REST API บน Cloud Run-based REST API
$ sed -i -e "s|CHANGE_ME|${RUN_CRUD_SERVICE_URL}|" app.yaml
หรือเปลี่ยนสตริง CHANGE_ME
ใน app.yaml
ด้วยตนเองด้วย URL ที่ถูกต้อง
env_variables:
RUN_CRUD_SERVICE_URL: CHANGE_ME
ไฟล์ Node.JS package.json
{
"name": "appengine-frontend",
"description": "Web frontend",
"license": "Apache-2.0",
"main": "index.js",
"engines": {
"node": "^14.0.0"
},
"dependencies": {
"express": "^4.17.1",
"isbn3": "^1.1.10"
},
"devDependencies": {
"nodemon": "^2.0.7"
},
"scripts": {
"start": "node index.js",
"dev": "nodemon --watch server --inspect index.js"
}
}
เราขอย้ำอีกครั้งว่าต้องการเรียกใช้แอปพลิเคชันนี้โดยใช้ Node.JS 14 เราใช้เฟรมเวิร์ก Express และโมดูล isbn3
NPM สำหรับการตรวจสอบความถูกต้องของหนังสือ รหัส ISBN
ในทรัพยากร Dependency ของการพัฒนา เราจะใช้โมดูล nodemon
เพื่อตรวจสอบการเปลี่ยนแปลงของไฟล์ แม้ว่าเราจะเรียกใช้แอปพลิเคชันในเครื่องด้วย npm start
ได้ แต่เปลี่ยนแปลงโค้ดบางอย่าง หยุดแอปด้วย ^C
แล้วเปิดอีกครั้ง แต่ก็ค่อนข้างน่าเบื่อ แต่เราสามารถใช้คำสั่งต่อไปนี้เพื่อให้แอปพลิเคชันโหลดซ้ำ / รีสตาร์ทโดยอัตโนมัติเมื่อมีการเปลี่ยนแปลง:
$ npm run dev
รหัส Node.JS index.js
const express = require('express');
const app = express();
app.use(express.static('public'));
const bodyParser = require('body-parser');
app.use(bodyParser.json());
เราต้องการเฟรมเวิร์กเว็บ Express เราระบุว่าไดเรกทอรีสาธารณะมีเนื้อหาแบบคงที่ซึ่งมิดเดิลแวร์ static
แสดง (อย่างน้อยเมื่อเรียกใช้ในเครื่องในโหมดการพัฒนา) ได้ สุดท้าย เรากำหนดให้ body-parser
แยกวิเคราะห์เพย์โหลด JSON
ลองมาดูเส้นทางที่เรากำหนดไว้กัน:
app.get('/', async (req, res) => {
res.redirect('/html/index.html');
});
app.get('/webapi', async (req, res) => {
res.send(process.env.RUN_CRUD_SERVICE_URL);
});
รายการแรกที่ตรงกัน /
จะเปลี่ยนเส้นทางไปยัง index.html
ในไดเรกทอรี public/html
ของเรา เราไม่ได้เรียกใช้ภายในรันไทม์ของ App Engine เช่นเดียวกับในโหมดนักพัฒนาซอฟต์แวร์ เราไม่รับการกำหนดเส้นทาง URL ของ App Engine ดังนั้นตรงนี้เราจะเปลี่ยนเส้นทาง URL รากไปยังไฟล์ HTML แทน
ปลายทางที่ 2 ที่เรากำหนด /webapi
จะแสดงผล URL ของ Cloud RUN REST API ด้วยวิธีนี้ โค้ด 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 เพียงพอแล้ว เราตรวจทานโค้ดใกล้เสร็จแล้ว เหลืออีกส่วนสุดท้ายคือ โค้ด JavaScript ฝั่งไคลเอ็นต์ app.js
โค้ดที่โต้ตอบกับ 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
— สตริงภาษาที่จะกรองตามภาษาเขียน
จากนั้นเราจะใช้ 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();
เพื่อให้รหัส ISBN น่าสนใจยิ่งขึ้น เราใช้ไลบรารี JsBarcode เพื่อสร้างบาร์โค้ดสวยๆ ให้เหมือนกับปกหลังของหนังสือจริง!
การเรียกใช้และทดสอบแอปพลิเคชันในเครื่อง
รหัสเพียงพอสำหรับตอนนี้ ได้เวลาที่แอปพลิเคชันทำงานแล้ว โดยก่อนอื่นเราจะดำเนินการในเครื่องภายใน 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 ในการสร้างส่วนหน้าของ API ทั่วไปในฟังก์ชันการนำเข้าข้อมูลและคอนเทนเนอร์ API ของ REST เพื่อเพิ่มฟีเจอร์ต่างๆ เช่น การจัดการคีย์ API เพื่อเข้าถึง API หรือกำหนดข้อจำกัดอัตราสำหรับผู้บริโภคที่ใช้ API
- ติดตั้งใช้งานโมดูลโหนด Swagger-UI ในแอปพลิเคชัน App Engine เพื่อจัดทำเอกสาร และเสนอพื้นที่ทดสอบสำหรับ REST API
- ในส่วนฟรอนท์เอนด์ที่นอกเหนือจากความสามารถในการท่องเว็บที่มีอยู่แล้ว ให้เพิ่มหน้าจอพิเศษเพื่อแก้ไขข้อมูล สร้างรายการหนังสือใหม่ นอกจากนี้ เนื่องจากเราใช้ฐานข้อมูล Cloud Firestore อยู่ จึงใช้ประโยชน์จากฟีเจอร์เรียลไทม์เพื่ออัปเดตข้อมูลหนังสือที่แสดงเมื่อมีการเปลี่ยนแปลง