เกี่ยวกับ Codelab นี้
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 ทั้งหมด (ชื่อด้านบนมีคนใช้แล้ว และจะใช้ไม่ได้ ขออภัย) และจะมีการอ้างอิงใน Codelab ว่า PROJECT_ID
ในภายหลัง
- ถัดไป หากต้องการใช้ทรัพยากร Google Cloud ให้เปิดใช้การเรียกเก็บเงินใน Cloud Console
การใช้งาน Codelab นี้น่าจะไม่มีค่าใช้จ่ายใดๆ หากมี โปรดตรวจสอบว่าคุณได้ทำตามวิธีการในส่วน "ล้างข้อมูล" ที่ตอนท้ายของ Codelab ซึ่งแนะนำวิธีการปิดทรัพยากร เพื่อไม่ให้เกิดการเรียกเก็บเงินนอกเหนือจากบทแนะนำนี้ ผู้ใช้ใหม่ของ Google Cloud จะมีสิทธิ์เข้าร่วมโปรแกรมทดลองใช้ฟรี$300 USD
Google Cloud Shell
แม้ Google Cloud จะทำงานจากระยะไกลจากแล็ปท็อปได้ แต่เราจะใช้ Google Cloud Shell ซึ่งเป็นสภาพแวดล้อมบรรทัดคำสั่งที่ทำงานในระบบคลาวด์ใน Codelab
เปิดใช้งาน Cloud Shell
- คลิกเปิดใช้งาน Cloud Shell
จาก Cloud Console
ครั้งแรกที่เปิด Cloud Shell คุณจะได้รับข้อความต้อนรับพร้อมคำอธิบาย หากเห็นข้อความต้อนรับ ให้คลิกต่อไป ข้อความต้อนรับจะไม่ปรากฏอีก ข้อความต้อนรับมีดังนี้
การจัดสรรและเชื่อมต่อกับ Cloud Shell ใช้เวลาเพียงไม่กี่นาที หลังจากเชื่อมต่อแล้ว คุณจะเห็นเทอร์มินัล Cloud Shell:
เครื่องเสมือนนี้เต็มไปด้วยเครื่องมือการพัฒนาทั้งหมดที่คุณต้องการ โดยมีไดเรกทอรีหลักขนาด 5 GB ที่ทำงานอย่างต่อเนื่องใน Google Cloud ซึ่งจะช่วยเพิ่มประสิทธิภาพของเครือข่ายและการตรวจสอบสิทธิ์ได้อย่างมาก งานทั้งหมดใน Codelab นี้ทำได้โดยใช้เบราว์เซอร์หรือ 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 และ API ส่วนเสริม
เปิดใช้ Cloud APIs
จาก 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.
สร้างอินสแตนซ์พื้นที่เก็บข้อมูล
จากนั้นเปิดใช้ 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 ในแท็บหรือหน้าต่างใหม่
- ข้าง "คอนโซล Google Cloud" คลิกลูกศรลง
แล้วเลือกโปรเจ็กต์
- คลิกเมนู
ที่มุมซ้ายบน
- คลิก API และ บริการ > ข้อมูลเข้าสู่ระบบ หน้าข้อมูลเข้าสู่ระบบสำหรับโปรเจ็กต์จะปรากฏขึ้น
- คลิกหน้าจอขอความยินยอม OAuth "หน้าจอขอความยินยอม OAuth" หน้าจอจะปรากฏขึ้น
- ในส่วน "ประเภทผู้ใช้" เลือกภายใน หากใช้บัญชี @gmail.com ให้เลือกภายนอก
- คลิกสร้าง "แก้ไขการลงทะเบียนแอป" จะปรากฏขึ้น
- กรอกแบบฟอร์ม:
- ในชื่อแอป ให้ป้อน "ส่วนเสริม Todo"
- ในอีเมลการสนับสนุนผู้ใช้ ให้ป้อนอีเมลส่วนตัวของคุณ
- ป้อนอีเมลส่วนตัวของคุณในส่วนข้อมูลติดต่อของนักพัฒนาแอป
- คลิกบันทึกและต่อไป แบบฟอร์มขอบเขตจะปรากฏขึ้น
- จากแบบฟอร์มขอบเขต ให้คลิกบันทึกและดำเนินการต่อ ข้อมูลสรุปจะปรากฏขึ้น
- คลิกกลับไปที่หน้าแดชบอร์ด
4 สร้างส่วนเสริมเริ่มต้น
เริ่มต้นโปรเจ็กต์
ในการเริ่มต้น ให้คุณสร้าง "สวัสดีโลก" แบบง่ายๆ แล้วติดตั้งใช้งาน ส่วนเสริมคือบริการบนเว็บที่ตอบสนองต่อคำขอ HTTPS และตอบกลับด้วยเพย์โหลด JSON ที่อธิบาย UI และการดำเนินการที่ต้องทำ ในส่วนเสริมนี้ คุณจะใช้ Node.js และเฟรมเวิร์ก Express
หากต้องการสร้างโปรเจ็กต์เทมเพลตนี้ ให้ใช้ Cloud Shell เพื่อสร้างไดเรกทอรีใหม่ชื่อ todo-add-on
และไปยังไดเรกทอรีดังกล่าว:
mkdir ~/todo-add-on cd ~/todo-add-on
คุณจะทำงานทั้งหมดให้กับ Codelab ในไดเรกทอรีนี้
เริ่มต้นโปรเจ็กต์ 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}`)
});
เซิร์ฟเวอร์จะไม่ได้ทำอะไรมากไปกว่าการแสดงข้อความ "สวัสดีโลก" ซึ่งไม่เป็นไร คุณจะเพิ่มฟังก์ชันการทำงานได้ในภายหลัง
ทำให้ใช้งานได้กับ 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
ใน Codelab นี้ คุณจะสร้างและทำให้ส่วนเสริมใช้งานได้หลายครั้งเมื่อมีการเพิ่มฟังก์ชันใหม่ แทนที่จะเรียกใช้คำสั่งแยกต่างหากเพื่อสร้างคอนเทนเนอร์ ให้พุชไปยังการลงทะเบียนคอนเทนเนอร์และทำให้ใช้งานได้กับ 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
บิลด์และการทำให้ใช้งานได้เต็มรูปแบบอาจใช้เวลา 2-3 นาที โดยเฉพาะในครั้งแรก
เมื่อบิลด์เสร็จสมบูรณ์ ให้ตรวจสอบว่าบริการใช้งานได้และค้นหา 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": {}
}
}
อัปโหลดข้อบ่งชี้การติดตั้งใช้งานโดยเรียกใช้คำสั่งดังนี้
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/] ในแท็บหรือหน้าต่างใหม่ มองหาส่วนเสริมที่มีไอคอนเครื่องหมายถูกทางด้านขวามือ
หากต้องการเปิดส่วนเสริม ให้คลิกไอคอนเครื่องหมายถูก ข้อความแจ้งให้ให้สิทธิ์ส่วนเสริมจะปรากฏขึ้น
คลิกให้สิทธิ์เข้าถึงและทำตามวิธีการให้สิทธิ์ในป๊อปอัป เมื่อดำเนินการเสร็จแล้ว ส่วนเสริมจะโหลดซ้ำโดยอัตโนมัติและแสดงข้อความ "สวัสดีทุกคน"
ยินดีด้วย ตอนนี้คุณมีส่วนเสริมง่ายๆ ที่ทำให้ใช้งานได้และติดตั้งไว้แล้ว ได้เวลาเปลี่ยนเป็นแอปพลิเคชันรายการงานแล้ว!
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');
จากนั้นเพิ่มเมธอด Helper เพื่อแยกวิเคราะห์โทเค็นรหัส ดังนี้
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
แสดงข้อมูลประจำตัวผู้ใช้
ซึ่งเป็นเวลาที่เหมาะแก่การจุดตรวจก่อนเพิ่มฟังก์ชันการทำงานของรายการงานทั้งหมด อัปเดตเส้นทางของแอปเพื่อพิมพ์อีเมลของผู้ใช้และรหัสที่ไม่ซ้ำกันแทนที่จะพิมพ์ "สวัสดีโลก"
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 ใช้รายการงาน
โมเดลข้อมูลเริ่มต้นสำหรับ Codelab นั้นตรงไปตรงมา นั่นคือรายการเอนทิตี Task
ซึ่งแต่ละรายการมีพร็อพเพอร์ตี้สำหรับข้อความอธิบายงานและการประทับเวลา
สร้างดัชนีพื้นที่เก็บข้อมูล
มีการเปิดใช้ 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
เพื่อใช้ "สิ่งที่ต้องทำ" เริ่มจากการนำเข้าไลบรารีพื้นที่เก็บข้อมูล แล้วสร้างไคลเอ็นต์
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 สำหรับ Codelab ประกอบด้วยการป้อนข้อความและรายการงานที่มีช่องทำเครื่องหมายสำหรับทำเครื่องหมายว่าเสร็จสิ้น โดยแต่ละบริการจะมีพร็อพเพอร์ตี้ 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 รายการ โดยรายการแรกสำหรับเพิ่มงานและอีก 1 รายการสำหรับลบงาน
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 บนแป้นพิมพ์ แล้วคลิกช่องทำเครื่องหมายเพื่อลบออก
หรือจะข้ามไปยังขั้นตอนสุดท้ายใน Codelab นี้และล้างโปรเจ็กต์ก็ได้หากต้องการ หรือหากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับส่วนเสริมต่อไป คุณต้องดำเนินการเพิ่มเติมอีก 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}`)
});
เพิ่มขอบเขตให้กับข้อบ่งชี้การทำให้ใช้งานได้
ก่อนสร้างเซิร์ฟเวอร์ใหม่ โปรดอัปเดตข้อบ่งชี้การติดตั้งใช้งานของส่วนเสริมให้รวมขอบเขต OAuth ของ https://www.googleapis.com/auth/drive.file
ไว้ด้วย อัปเดต 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
เมื่อดำเนินการเสร็จแล้ว ให้เปิด doc.new แทนการเปิด Gmail ให้เปิดเอกสาร Google ที่มีอยู่หรือสร้างเอกสารใหม่ หากสร้างเอกสารใหม่ โปรดป้อนข้อความหรือตั้งชื่อไฟล์
เปิดส่วนเสริม โดยส่วนเสริมจะแสดงปุ่มให้สิทธิ์เข้าถึงไฟล์ที่ด้านล่างของส่วนเสริม คลิกปุ่ม แล้วอนุญาตการเข้าถึงไฟล์
เมื่อให้สิทธิ์แล้ว ให้เพิ่มงานขณะอยู่ในเครื่องมือแก้ไข โดยงานจะมีป้ายกำกับที่ระบุว่ามีการแนบเอกสารแล้ว การคลิกลิงก์จะเปิดเอกสารในแท็บใหม่ แน่นอน การเปิดเอกสารที่คุณได้เปิดไว้อาจดูประหลาดๆ หากต้องการเพิ่มประสิทธิภาพ UI เพื่อกรองลิงก์สำหรับเอกสารปัจจุบันออก โปรดพิจารณาเครดิตเพิ่มเติมดังกล่าว
8 ขอแสดงความยินดี
ยินดีด้วย คุณสร้างและทำให้ส่วนเสริม Google Workspace ใช้งานได้โดยใช้ Cloud Run เรียบร้อยแล้ว แม้ Codelab จะกล่าวถึงแนวคิดหลักมากมายในการสร้างส่วนเสริม แต่ก็ยังมีสิ่งต่างๆ ให้สำรวจอีกมากมาย ดูแหล่งข้อมูลด้านล่างและอย่าลืมล้างโปรเจ็กต์เพื่อหลีกเลี่ยงการเรียกเก็บเงินเพิ่มเติม
ล้างข้อมูล
หากต้องการถอนการติดตั้งส่วนเสริมออกจากบัญชี ให้เรียกใช้คำสั่งนี้ใน Cloud Shell
gcloud workspace-add-ons deployments uninstall todo-add-on
โปรดทำดังนี้เพื่อเลี่ยงไม่ให้เกิดการเรียกเก็บเงินกับบัญชี Google Cloud Platform สำหรับทรัพยากรที่ใช้ในบทแนะนำนี้
- ใน Cloud Console ให้ไปที่หน้าจัดการทรัพยากร คลิกที่มุมซ้ายบน ให้คลิกเมนู
> IAM และ ผู้ดูแลระบบ > จัดการทรัพยากร
- ในรายการโปรเจ็กต์ ให้เลือกโปรเจ็กต์ของคุณ แล้วคลิกลบ
- ในกล่องโต้ตอบ ให้พิมพ์รหัสโปรเจ็กต์แล้วคลิกปิดเครื่องเพื่อลบโปรเจ็กต์
ดูข้อมูลเพิ่มเติม
- ภาพรวมส่วนเสริมของ Google Workspace
- ค้นหาแอปและส่วนเสริมที่มีอยู่ในมาร์เก็ตเพลส