1. บทนำ
ส่วนเสริมของ Google Workspace คือแอปพลิเคชันที่ปรับแต่งซึ่งผสานรวมกับแอปพลิเคชัน Google Workspace เช่น Gmail, เอกสาร, ชีต และสไลด์ ซึ่งช่วยให้นักพัฒนาแอปสร้างอินเทอร์เฟซผู้ใช้ที่กำหนดเองซึ่งผสานรวมเข้ากับ Google Workspace ได้โดยตรง ส่วนเสริมช่วยให้ผู้ใช้ทำงานได้อย่างมีประสิทธิภาพมากขึ้นโดยไม่ต้องสลับบริบท
ใน Codelab นี้ คุณจะได้เรียนรู้วิธีสร้างและทำให้ส่วนเสริมรายการงานอย่างง่ายใช้งานได้โดยใช้ Node.js, Cloud Run และ Datastore
สิ่งที่คุณจะได้เรียนรู้
- ใช้ Cloud Shell
- ทำให้ใช้งานได้กับ Cloud Run
- สร้างและติดตั้งใช้งานตัวอธิบายการติดตั้งใช้งานส่วนเสริม
- สร้าง UI ของส่วนเสริมด้วยเฟรมเวิร์กการ์ด
- ตอบสนองต่อการโต้ตอบของผู้ใช้
- ใช้ประโยชน์จากบริบทของผู้ใช้ในส่วนเสริม
2. การตั้งค่าและข้อกำหนด
ทำตามวิธีการตั้งค่าเพื่อสร้างโปรเจ็กต์ Google Cloud และเปิดใช้ API และบริการที่ส่วนเสริมจะใช้
การตั้งค่าสภาพแวดล้อมแบบเรียนรู้ด้วยตนเอง
- เปิด Cloud Console แล้วสร้างโปรเจ็กต์ใหม่ (หากยังไม่มีบัญชี Gmail หรือ Google Workspace ให้สร้างบัญชี)
โปรดจดจำรหัสโปรเจ็กต์ ซึ่งเป็นชื่อที่ไม่ซ้ำกันในโปรเจ็กต์ Google Cloud ทั้งหมด (ชื่อด้านบนมีผู้ใช้แล้วและจะใช้ไม่ได้ ขออภัย) ซึ่งจะเรียกว่า PROJECT_ID ในภายหลังใน Codelab นี้
- จากนั้นเปิดใช้การเรียกเก็บเงินใน Cloud Console เพื่อใช้ทรัพยากร Google Cloud
การทำตาม Codelab นี้ไม่ควรมีค่าใช้จ่ายมากนัก หรืออาจไม่มีเลย อย่าลืมทำตามวิธีการในส่วน "ล้างข้อมูล" ที่ตอนท้ายของ Codelab ซึ่งจะแนะนำวิธีปิดแหล่งข้อมูลเพื่อไม่ให้มีการเรียกเก็บเงินนอกเหนือจากบทแนะนำนี้ ผู้ใช้ Google Cloud รายใหม่มีสิทธิ์เข้าร่วมโปรแกรมช่วงทดลองใช้ฟรีมูลค่า$300 USD
Google Cloud Shell
แม้ว่าคุณจะใช้งาน Google Cloud จากแล็ปท็อปได้จากระยะไกล แต่ใน Codelab นี้เราจะใช้ Google Cloud Shell ซึ่งเป็นสภาพแวดล้อมบรรทัดคำสั่งที่ทำงานในระบบคลาวด์
เปิดใช้งาน Cloud Shell
- จาก Cloud Console ให้คลิกเปิดใช้งาน Cloud Shell
เมื่อเปิด Cloud Shell เป็นครั้งแรก คุณจะเห็นข้อความต้อนรับที่อธิบาย หากเห็นข้อความต้อนรับ ให้คลิกต่อไป ข้อความต้อนรับจะไม่ปรากฏขึ้นอีก ข้อความต้อนรับมีดังนี้
การจัดสรรและเชื่อมต่อกับ Cloud Shell จะใช้เวลาไม่นาน หลังจากเชื่อมต่อแล้ว คุณจะเห็นเทอร์มินัล Cloud Shell ดังนี้
เครื่องเสมือนนี้มีเครื่องมือพัฒนาซอฟต์แวร์ทั้งหมดที่คุณต้องการ โดยมีไดเรกทอรีหลักแบบถาวรขนาด 5 GB และทำงานใน Google Cloud ซึ่งช่วยเพิ่มประสิทธิภาพเครือข่ายและการตรวจสอบสิทธิ์ได้อย่างมาก คุณสามารถทำงานทั้งหมดในโค้ดแล็บนี้ได้ด้วยเบราว์เซอร์หรือ Chromebook
เมื่อเชื่อมต่อกับ Cloud Shell แล้ว คุณควรเห็นว่าคุณได้รับการตรวจสอบสิทธิ์แล้วและโปรเจ็กต์ได้รับการตั้งค่าเป็นรหัสโปรเจ็กต์ของคุณแล้ว
- เรียกใช้คำสั่งต่อไปนี้ใน Cloud Shell เพื่อยืนยันว่าคุณได้รับการตรวจสอบสิทธิ์แล้ว
gcloud auth list
หากได้รับแจ้งให้อนุญาต Cloud Shell เพื่อทำการเรียก API ของ GCP ให้คลิกให้สิทธิ์
เอาต์พุตของคำสั่ง
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com>
หากต้องการตั้งค่าบัญชีที่ใช้งานอยู่ ให้เรียกใช้คำสั่งต่อไปนี้
gcloud config set account <ACCOUNT>
หากต้องการยืนยันว่าคุณได้เลือกโปรเจ็กต์ที่ถูกต้องแล้ว ให้เรียกใช้คำสั่งต่อไปนี้ใน Cloud Shell
gcloud config list project
เอาต์พุตของคำสั่ง
[core] project = <PROJECT_ID>
หากระบบไม่แสดงโปรเจ็กต์ที่ถูกต้อง คุณสามารถตั้งค่าได้โดยใช้คำสั่งนี้
gcloud config set project <PROJECT_ID>
เอาต์พุตของคำสั่ง
Updated property [core/project].
Codelab นี้ใช้การดำเนินการบรรทัดคำสั่งและการแก้ไขไฟล์ร่วมกัน สำหรับการแก้ไขไฟล์ คุณสามารถใช้โปรแกรมแก้ไขโค้ดในตัวใน Cloud Shell ได้โดยคลิกปุ่มเปิดโปรแกรมแก้ไขทางด้านขวาของแถบเครื่องมือ Cloud Shell นอกจากนี้ คุณยังจะเห็นโปรแกรมแก้ไขยอดนิยม เช่น vim และ emacs ใน Cloud Shell ด้วย
3. เปิดใช้ Cloud Run, Datastore และ Add-on API
เปิดใช้ Cloud API
จาก Cloud Shell ให้เปิดใช้ Cloud API สำหรับคอมโพเนนต์ที่จะใช้
gcloud services enable \ run.googleapis.com \ cloudbuild.googleapis.com \ cloudresourcemanager.googleapis.com \ datastore.googleapis.com \ gsuiteaddons.googleapis.com
การดำเนินการนี้อาจใช้เวลาสักครู่จึงจะเสร็จสมบูรณ์
เมื่อเสร็จสมบูรณ์แล้ว ข้อความแสดงความสำเร็จที่คล้ายกับข้อความนี้จะปรากฏขึ้น
Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.
สร้างอินสแตนซ์ Datastore
จากนั้นเปิดใช้ App Engine และสร้างฐานข้อมูล Datastore การเปิดใช้ App Engine เป็นข้อกำหนดเบื้องต้นในการใช้ Datastore แต่เราจะไม่ใช้ App Engine เพื่อวัตถุประสงค์อื่นใด
gcloud app create --region=us-central gcloud firestore databases create --type=datastore-mode --region=us-central
สร้างหน้าจอขอความยินยอม OAuth
ส่วนเสริมนี้ต้องได้รับสิทธิ์จากผู้ใช้เพื่อเรียกใช้และดำเนินการกับข้อมูลของผู้ใช้ กำหนดค่าหน้าจอคำยินยอมของโปรเจ็กต์เพื่อเปิดใช้ สำหรับ Codelab คุณจะต้องกำหนดค่าหน้าจอขอความยินยอมเป็นแอปพลิเคชันภายใน ซึ่งหมายความว่าแอปพลิเคชันนี้ไม่ได้มีไว้สำหรับการเผยแพร่ต่อสาธารณะ เพื่อเริ่มต้นใช้งาน
- เปิด Google Cloud Console ในแท็บหรือหน้าต่างใหม่
- คลิกลูกศรลง
ข้าง "Google Cloud Console" แล้วเลือกโปรเจ็กต์ - คลิกเมนู
ที่มุมซ้ายบน - คลิก API และบริการ > ข้อมูลเข้าสู่ระบบ หน้าข้อมูลเข้าสู่ระบบสำหรับโปรเจ็กต์จะปรากฏขึ้น
- คลิกหน้าจอขอความยินยอม OAuth หน้าจอ "หน้าจอขอความยินยอม OAuth" จะปรากฏขึ้น
- เลือกภายในในส่วน "ประเภทผู้ใช้" หากใช้บัญชี @gmail.com ให้เลือกภายนอก
- คลิกสร้าง หน้า "แก้ไขการลงทะเบียนแอป" จะปรากฏขึ้น
- กรอกแบบฟอร์ม:
- ในชื่อแอป ให้ป้อน "ส่วนเสริม Todo"
- ในอีเมลสนับสนุนผู้ใช้ ให้ป้อนอีเมลส่วนตัว
- ในส่วนข้อมูลติดต่อของนักพัฒนาแอป ให้ป้อนอีเมลส่วนตัว
- คลิกบันทึกและต่อไป แบบฟอร์มขอบเขตจะปรากฏขึ้น
- จากแบบฟอร์มขอบเขต ให้คลิกบันทึกและดำเนินการต่อ ข้อมูลสรุปจะปรากฏขึ้น
- คลิกกลับไปที่แดชบอร์ด
4. สร้างส่วนเสริมเริ่มต้น
เริ่มต้นโปรเจ็กต์
ในการเริ่มต้น คุณจะต้องสร้างและติดตั้งใช้งานส่วนเสริม "Hello world" แบบง่าย ส่วนเสริมคือบริการเว็บที่ตอบสนองต่อคำขอ https และตอบสนองด้วยเพย์โหลด JSON ที่อธิบาย UI และการดำเนินการที่จะทำ ในส่วนเสริมนี้ คุณจะใช้ Node.js และเฟรมเวิร์ก Express
หากต้องการสร้างโปรเจ็กต์เทมเพลตนี้ ให้ใช้ Cloud Shell เพื่อสร้างไดเรกทอรีใหม่ชื่อ todo-add-on แล้วไปที่ไดเรกทอรีดังกล่าว
mkdir ~/todo-add-on cd ~/todo-add-on
คุณจะทำงานทั้งหมดสำหรับโค้ดแล็บในไดเรกทอรีนี้
เริ่มต้นโปรเจ็กต์ Node.js โดยใช้คำสั่งต่อไปนี้
npm init
NPM จะถามคำถามหลายข้อเกี่ยวกับการกำหนดค่าโปรเจ็กต์ เช่น ชื่อและเวอร์ชัน สำหรับคำถามแต่ละข้อ ให้กด ENTER เพื่อยอมรับค่าเริ่มต้น จุดแรกเข้าเริ่มต้นคือไฟล์ชื่อ index.js ซึ่งเราจะสร้างในขั้นตอนถัดไป
จากนั้นติดตั้งเฟรมเวิร์กเว็บ Express โดยทำดังนี้
npm install --save express express-async-handler
สร้างแบ็กเอนด์ของส่วนเสริม
ถึงเวลาเริ่มสร้างแอปแล้ว
สร้างไฟล์ชื่อ index.js หากต้องการสร้างไฟล์ ให้ใช้ Cloud Shell Editor โดยคลิกปุ่มเปิดตัวแก้ไขบนแถบเครื่องมือของหน้าต่าง Cloud Shell หรือจะแก้ไขและจัดการไฟล์ใน Cloud Shell โดยใช้ vim หรือ emacs ก็ได้
หลังจากสร้างไฟล์ index.js แล้ว ให้เพิ่มเนื้อหาต่อไปนี้
const express = require('express');
const asyncHandler = require('express-async-handler');
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post("/", asyncHandler(async (req, res) => {
const card = {
sections: [{
widgets: [
{
textParagraph: {
text: `Hello world!`
}
},
]
}]
};
const renderAction = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(renderAction);
}));
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
เซิร์ฟเวอร์ไม่ได้ทำอะไรมากไปกว่าการแสดงข้อความ "Hello world" ซึ่งก็ถือว่าใช้ได้ คุณจะเพิ่มฟังก์ชันการทำงานเพิ่มเติมได้ในภายหลัง
ทำให้ใช้งานได้กับ Cloud Run
หากต้องการทําให้ใช้งานได้ใน Cloud Run คุณต้องสร้างคอนเทนเนอร์ของแอป
สร้างคอนเทนเนอร์
สร้าง Dockerfile ชื่อ Dockerfile ที่มีเนื้อหาต่อไปนี้
FROM node:12-slim
# Create and change to the app directory.
WORKDIR /usr/src/app
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./
# Install production dependencies.
# If you add a package-lock.json, speed your build by switching to 'npm ci'.
# RUN npm ci --only=production
RUN npm install --only=production
# Copy local code to the container image.
COPY . ./
# Run the web service on container startup.
CMD [ "node", "index.js" ]
ป้องกันไม่ให้ไฟล์ที่ไม่ต้องการอยู่ในคอนเทนเนอร์
สร้าง.dockerignoreที่มีเนื้อหาต่อไปนี้เพื่อช่วยให้คอนเทนเนอร์มีขนาดเล็ก
Dockerfile
.dockerignore
node_modules
npm-debug.log
เปิดใช้ Cloud Build
ในโค้ดแล็บนี้ คุณจะได้สร้างและติดตั้งใช้งานส่วนเสริมหลายครั้งเมื่อมีการเพิ่มฟังก์ชันการทำงานใหม่ แทนที่จะเรียกใช้คำสั่งแยกต่างหากเพื่อสร้างคอนเทนเนอร์ พุชไปยัง Container Registry และทำให้ใช้งานได้ใน Cloud Build ให้ใช้ Cloud Build เพื่อจัดระเบียบขั้นตอน สร้างไฟล์ cloudbuild.yaml ที่มีวิธีการสร้างและติดตั้งใช้งานแอปพลิเคชัน
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME', '.']
# Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME']
# Deploy container image to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- '$_SERVICE_NAME'
- '--image'
- 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
- '--region'
- '$_REGION'
- '--platform'
- 'managed'
images:
- 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
substitutions:
_SERVICE_NAME: todo-add-on
_REGION: us-central1
เรียกใช้คำสั่งต่อไปนี้เพื่อให้สิทธิ์ Cloud Build ในการติดตั้งใช้งานแอป
PROJECT_ID=$(gcloud config list --format='value(core.project)')
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
--role=roles/run.admin
gcloud iam service-accounts add-iam-policy-binding \
$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
--member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
--role=roles/iam.serviceAccountUser
สร้างและติดตั้งใช้งานแบ็กเอนด์ของส่วนเสริม
หากต้องการเริ่มบิลด์ ให้เรียกใช้คำสั่งต่อไปนี้ใน Cloud Shell
gcloud builds submit
การสร้างและติดตั้งใช้งานทั้งหมดอาจใช้เวลาสักครู่จึงจะเสร็จสมบูรณ์ โดยเฉพาะอย่างยิ่งในครั้งแรก
เมื่อการสร้างเสร็จสมบูรณ์แล้ว ให้ยืนยันว่าได้ติดตั้งใช้งานบริการแล้วและค้นหา URL เรียกใช้คำสั่งต่อไปนี้
gcloud run services list --platform managed
คัดลอก URL นี้ คุณจะต้องใช้ในขั้นตอนถัดไป ซึ่งเป็นการบอก Google Workspace วิธีเรียกใช้ส่วนเสริม
ลงทะเบียนส่วนเสริม
เมื่อเซิร์ฟเวอร์พร้อมใช้งานแล้ว ให้เขียนคำอธิบายส่วนเสริมเพื่อให้ Google Workspace ทราบวิธีแสดงและเรียกใช้ส่วนเสริม
สร้างตัวอธิบายการทำให้ใช้งานได้
สร้างไฟล์ deployment.json ที่มีเนื้อหาต่อไปนี้ อย่าลืมใช้ URL ของแอปที่ติดตั้งใช้งานแทนตัวยึดตำแหน่ง URL
{
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute"
],
"addOns": {
"common": {
"name": "Todo Codelab",
"logoUrl": "https://raw.githubusercontent.com/webdog/octicons-png/main/black/check.png",
"homepageTrigger": {
"runFunction": "URL"
}
},
"gmail": {},
"drive": {},
"calendar": {},
"docs": {},
"sheets": {},
"slides": {}
}
}
อัปโหลด Deployment Descriptor โดยเรียกใช้คำสั่งต่อไปนี้
gcloud workspace-add-ons deployments create todo-add-on --deployment-file=deployment.json
ให้สิทธิ์เข้าถึงแบ็กเอนด์ของส่วนเสริม
นอกจากนี้ เฟรมเวิร์กของส่วนเสริมยังต้องมีสิทธิ์ในการเรียกใช้บริการด้วย เรียกใช้คำสั่งต่อไปนี้เพื่ออัปเดตนโยบาย IAM สำหรับ Cloud Run เพื่ออนุญาตให้ Google Workspace เรียกใช้ส่วนเสริม
SERVICE_ACCOUNT_EMAIL=$(gcloud workspace-add-ons get-authorization --format="value(serviceAccountEmail)")
gcloud run services add-iam-policy-binding todo-add-on --platform managed --region us-central1 --role roles/run.invoker --member "serviceAccount:$SERVICE_ACCOUNT_EMAIL"
ติดตั้งส่วนเสริมสำหรับการทดสอบ
หากต้องการติดตั้งส่วนเสริมในโหมดการพัฒนาสำหรับบัญชี ให้เรียกใช้คำสั่งต่อไปนี้ใน Cloud Shell
gcloud workspace-add-ons deployments install todo-add-on
เปิด (Gmail)[https://mail.google.com/] ในแท็บหรือหน้าต่างใหม่ ทางด้านขวามือ ให้ค้นหาส่วนเสริมที่มีไอคอนเครื่องหมายถูก

หากต้องการเปิดส่วนเสริม ให้คลิกไอคอนเครื่องหมายถูก ข้อความแจ้งให้สิทธิ์ส่วนเสริมจะปรากฏขึ้น

คลิกให้สิทธิ์เข้าถึง แล้วทำตามวิธีการในขั้นตอนการให้สิทธิ์ในป๊อปอัป เมื่อเสร็จแล้ว ส่วนเสริมจะโหลดซ้ำโดยอัตโนมัติและแสดงข้อความ "Hello world!"
ยินดีด้วย ตอนนี้คุณได้ติดตั้งและใช้งานส่วนเสริมแบบง่ายๆ แล้ว ถึงเวลาเปลี่ยนให้เป็นแอปพลิเคชันรายการงานแล้ว
5. เข้าถึงข้อมูลประจำตัวของผู้ใช้
โดยปกติแล้ว ผู้ใช้จำนวนมากจะใช้ส่วนเสริมเพื่อทำงานกับข้อมูลที่เป็นส่วนตัวสำหรับตนเองหรือองค์กร ใน Codelab นี้ ส่วนเสริมควรแสดงเฉพาะงานของผู้ใช้ปัจจุบัน ระบบจะส่งข้อมูลประจำตัวของผู้ใช้ไปยังส่วนเสริมผ่านโทเค็นข้อมูลประจำตัวที่ต้องถอดรหัส
เพิ่มขอบเขตไปยังตัวอธิบายการติดตั้งใช้งาน
ระบบจะไม่ส่งข้อมูลประจำตัวของผู้ใช้โดยค่าเริ่มต้น เนื่องจากเป็นข้อมูลผู้ใช้และส่วนเสริมต้องมีสิทธิ์เข้าถึง หากต้องการรับสิทธิ์ดังกล่าว ให้อัปเดต deployment.json และเพิ่มขอบเขต OAuth ของ openid และ email ลงในรายการขอบเขตที่ส่วนเสริมต้องการ หลังจากเพิ่มขอบเขต OAuth แล้ว ส่วนเสริมจะแจ้งให้ผู้ใช้ให้สิทธิ์เข้าถึงในครั้งถัดไปที่ใช้ส่วนเสริม
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute",
"openid",
"email"
],
จากนั้นใน Cloud Shell ให้เรียกใช้คำสั่งนี้เพื่ออัปเดตตัวอธิบายการติดตั้งใช้งาน
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
อัปเดตเซิร์ฟเวอร์ส่วนเสริม
แม้ว่าส่วนเสริมจะได้รับการกำหนดค่าให้ขอข้อมูลระบุตัวตนของผู้ใช้ แต่ก็ยังต้องอัปเดตการติดตั้งใช้งานอยู่
แยกวิเคราะห์โทเค็นข้อมูลประจำตัว
เริ่มต้นด้วยการเพิ่มไลบรารีการตรวจสอบสิทธิ์ของ Google ลงในโปรเจ็กต์โดยทำดังนี้
npm install --save google-auth-library
จากนั้นแก้ไข index.js เพื่อกำหนดให้ต้องมี OAuth2Client ดังนี้
const { OAuth2Client } = require('google-auth-library');
จากนั้นเพิ่มเมธอดตัวช่วยเพื่อแยกวิเคราะห์โทเค็นรหัส
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
แสดงข้อมูลระบุตัวตนของผู้ใช้
ตอนนี้เป็นเวลาที่เหมาะสมในการตรวจสอบก่อนที่จะเพิ่มฟังก์ชันการทำงานทั้งหมดของรายการงาน อัปเดตเส้นทางของแอปเพื่อพิมพ์อีเมลและรหัสที่ไม่ซ้ำของผู้ใช้แทนข้อความ "Hello world"
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const card = {
sections: [{
widgets: [
{
textParagraph: {
text: `Hello ${user.email} ${user.sub}`
}
},
]
}]
};
const renderAction = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(renderAction);
}));
หลังจากทำการเปลี่ยนแปลงเหล่านี้แล้ว ไฟล์ index.js ที่ได้ควรมีลักษณะดังนี้
const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const card = {
sections: [{
widgets: [
{
textParagraph: {
text: `Hello ${user.email} ${user.sub}`
}
},
]
}]
};
const renderAction = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(renderAction);
}));
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
ติดตั้งใช้งานอีกครั้งและทดสอบ
สร้างและติดตั้งใช้งานส่วนเสริมอีกครั้ง จาก Cloud Shell ให้เรียกใช้คำสั่งต่อไปนี้
gcloud builds submit
เมื่อติดตั้งเซิร์ฟเวอร์ใหม่แล้ว ให้เปิดหรือโหลด Gmail ซ้ำ แล้วเปิดส่วนเสริมอีกครั้ง เนื่องจากขอบเขตมีการเปลี่ยนแปลง ส่วนเสริมจึงจะขอให้คุณให้สิทธิ์อีกครั้ง ให้สิทธิ์ส่วนเสริมอีกครั้ง เมื่อเสร็จแล้ว ส่วนเสริมจะแสดงอีเมลและรหัสผู้ใช้ของคุณ
ตอนนี้ส่วนเสริมรู้แล้วว่าผู้ใช้คือใคร คุณจึงเริ่มเพิ่มฟังก์ชันรายการงานได้
6. ใช้รายการงาน
โมเดลข้อมูลเริ่มต้นสำหรับโค้ดแล็บนั้นตรงไปตรงมา นั่นคือรายการTaskเอนทิตี ซึ่งแต่ละรายการมีพร็อพเพอร์ตี้สำหรับข้อความอธิบายงานและไทม์สแตมป์
สร้างดัชนี Datastore
เราได้เปิดใช้ Datastore สำหรับโปรเจ็กต์แล้วก่อนหน้านี้ใน Codelab ไม่จำเป็นต้องมีสคีมา แต่ต้องสร้างดัชนีสำหรับการค้นหาแบบผสมอย่างชัดแจ้ง การสร้างดัชนีอาจใช้เวลาสักครู่ ดังนั้นคุณจะต้องสร้างดัชนีก่อน
สร้างไฟล์ชื่อ index.yaml ที่มีเนื้อหาต่อไปนี้
indexes:
- kind: Task
ancestor: yes
properties:
- name: created
จากนั้นอัปเดตดัชนี Datastore โดยทำดังนี้
gcloud datastore indexes create index.yaml
เมื่อได้รับแจ้งให้ดำเนินการต่อ ให้กด ENTER บนแป้นพิมพ์ การสร้างดัชนีจะเกิดขึ้นในเบื้องหลัง ในระหว่างนี้ ให้เริ่มอัปเดตโค้ดส่วนเสริมเพื่อใช้ "สิ่งที่ต้องทำ"
อัปเดตแบ็กเอนด์ของส่วนเสริม
ติดตั้งไลบรารี Datastore ลงในโปรเจ็กต์โดยทำดังนี้
npm install --save @google-cloud/datastore
อ่านและเขียนไปยัง Datastore
อัปเดต index.js เพื่อใช้ "todos" โดยเริ่มจากการนำเข้าไลบรารี Datastore และสร้างไคลเอ็นต์ ดังนี้
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
เพิ่มเมธอดเพื่ออ่านและเขียนงานจาก Datastore ดังนี้
async function listTasks(userId) {
const parentKey = datastore.key(['User', userId]);
const query = datastore.createQuery('Task')
.hasAncestor(parentKey)
.order('created')
.limit(20);
const [tasks] = await datastore.runQuery(query);
return tasks;;
}
async function addTask(userId, task) {
const key = datastore.key(['User', userId, 'Task']);
const entity = {
key,
data: task,
};
await datastore.save(entity);
return entity;
}
async function deleteTasks(userId, taskIds) {
const keys = taskIds.map(id => datastore.key(['User', userId,
'Task', datastore.int(id)]));
await datastore.delete(keys);
}
ใช้การแสดงผล UI
การเปลี่ยนแปลงส่วนใหญ่เป็นการเปลี่ยนแปลง UI ของส่วนเสริม ก่อนหน้านี้ การ์ดทั้งหมดที่ UI แสดงผลจะเป็นแบบคงที่ ซึ่งจะไม่เปลี่ยนแปลงตามข้อมูลที่มี ในที่นี้ การ์ดจะต้องสร้างแบบไดนามิกโดยอิงตามรายการงานปัจจุบันของผู้ใช้
UI ของโค้ดแล็บประกอบด้วยช่องป้อนข้อความพร้อมกับรายการงานที่มีช่องทำเครื่องหมายเพื่อทำเครื่องหมายว่าเสร็จสมบูรณ์ แต่ละรายการยังมีพร็อพเพอร์ตี้ onChangeAction ซึ่งทำให้เกิดการเรียกกลับไปยังเซิร์ฟเวอร์ส่วนเสริมเมื่อผู้ใช้เพิ่มหรือลบงาน ในแต่ละกรณีเหล่านี้ ระบบจะต้องแสดง UI อีกครั้งพร้อมกับรายการงานที่อัปเดต เราจึงขอแนะนำวิธีใหม่ในการสร้าง UI ของการ์ดเพื่อจัดการปัญหานี้
แก้ไข index.js ต่อไปและเพิ่มวิธีการต่อไปนี้
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
// Input for adding a new task
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
// Create text & checkbox for each task
tasks.forEach(task => taskListSection.widgets.push({
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
}));
} else {
// Placeholder for empty task list
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
}
return card;
}
อัปเดตเส้นทาง
ตอนนี้มีเมธอดตัวช่วยในการอ่านและเขียนไปยัง Datastore รวมถึงสร้าง UI แล้ว เรามาเชื่อมต่อเมธอดเหล่านี้ในเส้นทางของแอปกัน แทนที่เส้นทางที่มีอยู่และเพิ่มอีก 2 เส้นทาง ได้แก่ เส้นทางสำหรับเพิ่มงานและเส้นทางสำหรับลบงาน
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(responsePayload);
}));
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date()
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/complete', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const completedTasks = formInputs.completedTasks;
if (!completedTasks || !completedTasks.stringInputs) {
return {};
}
await deleteTasks(user.sub, completedTasks.stringInputs.value);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task completed.'
},
}
}
};
res.json(responsePayload);
}));
นี่คือไฟล์ index.js ที่ใช้งานได้อย่างสมบูรณ์ฉบับสุดท้าย
const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(responsePayload);
}));
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date()
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/complete', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const completedTasks = formInputs.completedTasks;
if (!completedTasks || !completedTasks.stringInputs) {
return {};
}
await deleteTasks(user.sub, completedTasks.stringInputs.value);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task completed.'
},
}
}
};
res.json(responsePayload);
}));
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
// Input for adding a new task
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
// Create text & checkbox for each task
tasks.forEach(task => taskListSection.widgets.push({
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
}));
} else {
// Placeholder for empty task list
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
}
return card;
}
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
async function listTasks(userId) {
const parentKey = datastore.key(['User', userId]);
const query = datastore.createQuery('Task')
.hasAncestor(parentKey)
.order('created')
.limit(20);
const [tasks] = await datastore.runQuery(query);
return tasks;;
}
async function addTask(userId, task) {
const key = datastore.key(['User', userId, 'Task']);
const entity = {
key,
data: task,
};
await datastore.save(entity);
return entity;
}
async function deleteTasks(userId, taskIds) {
const keys = taskIds.map(id => datastore.key(['User', userId,
'Task', datastore.int(id)]));
await datastore.delete(keys);
}
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
ติดตั้งใช้งานอีกครั้งและทดสอบ
หากต้องการสร้างและติดตั้งใช้งานส่วนเสริมอีกครั้ง ให้เริ่มการสร้าง ใน Cloud Shell ให้เรียกใช้คำสั่งต่อไปนี้
gcloud builds submit
ใน Gmail ให้โหลดส่วนเสริมซ้ำ แล้ว UI ใหม่จะปรากฏขึ้น โปรดสละเวลาสักครู่เพื่อสำรวจส่วนเสริม เพิ่มงาน 2-3 รายการโดยป้อนข้อความลงในช่องป้อนข้อมูลแล้วกด Enter บนแป้นพิมพ์ จากนั้นคลิกช่องทำเครื่องหมายเพื่อลบงาน

หากต้องการ คุณสามารถข้ามไปยังขั้นตอนสุดท้ายในโค้ดแล็บนี้และล้างข้อมูลโปรเจ็กต์ได้ หรือหากต้องการดูข้อมูลเพิ่มเติมเกี่ยวกับส่วนเสริมต่อไป คุณสามารถทำตามขั้นตอนอีก 1 ขั้นตอนได้
7. (ไม่บังคับ) การเพิ่มบริบท
ฟีเจอร์ที่มีประสิทธิภาพมากที่สุดอย่างหนึ่งของส่วนเสริมคือการรับรู้บริบท ส่วนเสริมสามารถเข้าถึงบริบทของ Google Workspace เช่น อีเมลที่ผู้ใช้กำลังดู กิจกรรมในปฏิทิน และเอกสารได้โดยได้รับสิทธิ์จากผู้ใช้ นอกจากนี้ ส่วนเสริมยังสามารถดำเนินการต่างๆ เช่น การแทรกเนื้อหาได้ด้วย ใน Codelab นี้ คุณจะได้เพิ่มการรองรับบริบทสำหรับเครื่องมือแก้ไข Workspace (เอกสาร ชีต และสไลด์) เพื่อแนบเอกสารปัจจุบันกับงานที่สร้างขึ้นขณะอยู่ในเครื่องมือแก้ไข เมื่อแสดงงานแล้ว การคลิกงานจะเปิดเอกสารในแท็บใหม่เพื่อนำผู้ใช้กลับไปยังเอกสารเพื่อทำงานให้เสร็จ
อัปเดตแบ็กเอนด์ของส่วนเสริม
อัปเดตเส้นทาง newTask
ก่อนอื่น ให้อัปเดตเส้นทาง /newTask เพื่อรวมรหัสเอกสารในงาน หากมี
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
// Get the current document if it is present
const editorInfo = event.docs || event.sheets || event.slides;
let document = null;
if (editorInfo && editorInfo.id) {
document = {
id: editorInfo.id,
}
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date(),
document,
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
ตอนนี้งานที่สร้างใหม่จะมีรหัสเอกสารปัจจุบัน อย่างไรก็ตาม ระบบจะไม่แชร์บริบทในเอดิเตอร์โดยค่าเริ่มต้น เช่นเดียวกับข้อมูลผู้ใช้รายอื่นๆ ผู้ใช้ต้องให้สิทธิ์แก่ส่วนเสริมเพื่อเข้าถึงข้อมูล แนวทางที่แนะนำคือการขอและให้สิทธิ์เป็นรายไฟล์ เพื่อป้องกันการแชร์ข้อมูลมากเกินไป
อัปเดต UI
ใน index.js ให้อัปเดต buildCard เพื่อทำการเปลี่ยนแปลง 2 อย่าง อย่างแรกคือการอัปเดตการแสดงผลของงานให้มีลิงก์ไปยังเอกสารหากมี ส่วนที่ 2 คือการแสดงข้อความแจ้งการให้สิทธิ์ที่ไม่บังคับหากมีการแสดงผลส่วนเสริมในเครื่องมือแก้ไขและยังไม่ได้ให้สิทธิ์เข้าถึงไฟล์
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
tasks.forEach(task => {
const widget = {
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
};
// Make item clickable and open attached doc if present
if (task.document) {
widget.decoratedText.bottomLabel = 'Click to open document.';
const id = task.document.id;
const url = `https://drive.google.com/open?id=${id}`
widget.decoratedText.onClick = {
openLink: {
openAs: 'FULL_SIZE',
onClose: 'NOTHING',
url: url,
}
}
}
taskListSection.widgets.push(widget)
});
} else {
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
};
// Display file authorization prompt if the host is an editor
// and no doc ID present
const event = req.body;
const editorInfo = event.docs || event.sheets || event.slides;
const showFileAuth = editorInfo && editorInfo.id === undefined;
if (showFileAuth) {
card.fixedFooter = {
primaryButton: {
text: 'Authorize file access',
onClick: {
action: {
function: `${baseUrl}/authorizeFile`,
}
}
}
}
}
return card;
}
ใช้เส้นทางการให้สิทธิ์ไฟล์
ปุ่มการให้สิทธิ์จะเพิ่มเส้นทางใหม่ไปยังแอป ดังนั้นเรามาติดตั้งใช้งานกัน เส้นทางนี้จะแนะนำแนวคิดใหม่ นั่นคือการดำเนินการของแอปโฮสต์ ซึ่งเป็นวิธีการพิเศษสำหรับการโต้ตอบกับแอปพลิเคชันโฮสต์ของส่วนเสริม ในกรณีนี้ ให้ขอสิทธิ์เข้าถึงไฟล์เอดิเตอร์ปัจจุบัน
ใน index.js ให้เพิ่มเส้นทาง /authorizeFile ดังนี้
app.post('/authorizeFile', asyncHandler(async (req, res) => {
const responsePayload = {
renderActions: {
hostAppAction: {
editorAction: {
requestFileScopeForActiveDocument: {}
}
},
}
};
res.json(responsePayload);
}));
นี่คือไฟล์ index.js ที่ใช้งานได้อย่างสมบูรณ์ฉบับสุดท้าย
const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(responsePayload);
}));
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
// Get the current document if it is present
const editorInfo = event.docs || event.sheets || event.slides;
let document = null;
if (editorInfo && editorInfo.id) {
document = {
id: editorInfo.id,
}
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date(),
document,
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/complete', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const completedTasks = formInputs.completedTasks;
if (!completedTasks || !completedTasks.stringInputs) {
return {};
}
await deleteTasks(user.sub, completedTasks.stringInputs.value);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task completed.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/authorizeFile', asyncHandler(async (req, res) => {
const responsePayload = {
renderActions: {
hostAppAction: {
editorAction: {
requestFileScopeForActiveDocument: {}
}
},
}
};
res.json(responsePayload);
}));
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
tasks.forEach(task => {
const widget = {
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
};
// Make item clickable and open attached doc if present
if (task.document) {
widget.decoratedText.bottomLabel = 'Click to open document.';
const id = task.document.id;
const url = `https://drive.google.com/open?id=${id}`
widget.decoratedText.onClick = {
openLink: {
openAs: 'FULL_SIZE',
onClose: 'NOTHING',
url: url,
}
}
}
taskListSection.widgets.push(widget)
});
} else {
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
};
// Display file authorization prompt if the host is an editor
// and no doc ID present
const event = req.body;
const editorInfo = event.docs || event.sheets || event.slides;
const showFileAuth = editorInfo && editorInfo.id === undefined;
if (showFileAuth) {
card.fixedFooter = {
primaryButton: {
text: 'Authorize file access',
onClick: {
action: {
function: `${baseUrl}/authorizeFile`,
}
}
}
}
}
return card;
}
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
async function listTasks(userId) {
const parentKey = datastore.key(['User', userId]);
const query = datastore.createQuery('Task')
.hasAncestor(parentKey)
.order('created')
.limit(20);
const [tasks] = await datastore.runQuery(query);
return tasks;;
}
async function addTask(userId, task) {
const key = datastore.key(['User', userId, 'Task']);
const entity = {
key,
data: task,
};
await datastore.save(entity);
return entity;
}
async function deleteTasks(userId, taskIds) {
const keys = taskIds.map(id => datastore.key(['User', userId,
'Task', datastore.int(id)]));
await datastore.delete(keys);
}
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
เพิ่มขอบเขตไปยังตัวอธิบายการติดตั้งใช้งาน
ก่อนสร้างเซิร์ฟเวอร์ใหม่ ให้อัปเดตตัวอธิบายการติดตั้งใช้งานส่วนเสริมให้มีhttps://www.googleapis.com/auth/drive.fileขอบเขต OAuth อัปเดต deployment.json เพื่อเพิ่ม https://www.googleapis.com/auth/drive.file ลงในรายการขอบเขตของ OAuth โดยทำดังนี้
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute",
"https://www.googleapis.com/auth/drive.file",
"openid",
"email"
]
อัปโหลดเวอร์ชันใหม่โดยเรียกใช้คำสั่ง Cloud Shell นี้
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
ติดตั้งใช้งานอีกครั้งและทดสอบ
สุดท้ายคือการสร้างเซิร์ฟเวอร์ใหม่ จาก Cloud Shell ให้เรียกใช้คำสั่งต่อไปนี้
gcloud builds submit
เมื่อเสร็จแล้ว ให้เปิดเอกสาร Google ที่มีอยู่หรือสร้างเอกสารใหม่โดยเปิด doc.new แทนการเปิด Gmail หากสร้างเอกสารใหม่ อย่าลืมป้อนข้อความหรือตั้งชื่อไฟล์
เปิดส่วนเสริม ส่วนเสริมจะแสดงปุ่มให้สิทธิ์เข้าถึงไฟล์ที่ด้านล่างของส่วนเสริม คลิกปุ่ม แล้วให้สิทธิ์เข้าถึงไฟล์
เมื่อได้รับอนุญาตแล้ว ให้เพิ่มงานขณะอยู่ในเครื่องมือแก้ไข งานจะมีป้ายกำกับที่ระบุว่ามีการแนบเอกสาร การคลิกลิงก์จะเปิดเอกสารในแท็บใหม่ แน่นอนว่าการเปิดเอกสารที่คุณเปิดอยู่แล้วอาจดูไม่สมเหตุสมผลนัก หากต้องการเพิ่มประสิทธิภาพ UI เพื่อกรองลิงก์สำหรับเอกสารปัจจุบัน โปรดพิจารณาว่าเป็นการให้คะแนนพิเศษ
8. ขอแสดงความยินดี
ยินดีด้วย คุณสร้างและทำให้ส่วนเสริม Google Workspace ใช้งานได้โดยใช้ Cloud Run เรียบร้อยแล้ว แม้ว่าโค้ดแล็บจะครอบคลุมแนวคิดหลักหลายอย่างในการสร้างส่วนเสริม แต่ก็ยังมีอีกหลายเรื่องที่ต้องศึกษา ดูแหล่งข้อมูลด้านล่างและอย่าลืมล้างข้อมูลในโปรเจ็กต์เพื่อหลีกเลี่ยงการเรียกเก็บเงินเพิ่มเติม
ล้างข้อมูล
หากต้องการถอนการติดตั้งส่วนเสริมออกจากบัญชี ให้เรียกใช้คำสั่งต่อไปนี้ใน Cloud Shell
gcloud workspace-add-ons deployments uninstall todo-add-on
โปรดดำเนินการดังนี้เพื่อเลี่ยงไม่ให้เกิดการเรียกเก็บเงินกับบัญชี Google Cloud Platform สำหรับทรัพยากรที่ใช้ในบทแนะนำนี้
- ใน Cloud Console ให้ไปที่หน้าจัดการทรัพยากร ที่มุมซ้ายบน ให้คลิกเมนู
> IAM และผู้ดูแลระบบ > จัดการทรัพยากร
- เลือกโปรเจ็กต์ในรายการโปรเจ็กต์ แล้วคลิกลบ
- ในกล่องโต้ตอบ ให้พิมพ์รหัสโปรเจ็กต์ แล้วคลิกปิดเพื่อลบโปรเจ็กต์
ดูข้อมูลเพิ่มเติม
- ภาพรวมส่วนเสริมของ Google Workspace
- ค้นหาแอปและส่วนเสริมที่มีอยู่ใน Marketplace