🥳終於可以使用Google Sheets API啦!
相比設置GCP,使用API超 ‧ 簡 ‧ 單~
昨天(Day27)已經取得了access token,今晚終於可以開始使用Google Sheets API。
用Deno執行TypeScript腳本,以Google Sheets API抓取指定Google Sheet的資料,並以JSON格式存放於專案目錄內,供Vue Component使用。
# Runtime Environments
├── Node.js (dev/build)
│ └── pnpm-managed toolchain
│ ├── Vite
│ ├── Vue
│ ├── TypeScript
│ └── Sass
└── Deno (prebuild)
├── get-gcp-token.ts (Day27)
└── fetch-sheet-data.ts (Day28)
既然是要抓取資料,直覺上就是找get的方法,於是在官方文件查到了spreadsheets.values.get這個方法。[^1]
它的API endpoint是:https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}/values/{range}
首先,稍微了解它會回傳什麼,並寫TypeScript文件:[^2]
interface GoogleSheetsResponse {
range: string;
majorDimension: string;
values?: string[][];
}
在這裡,當我使用spreadsheets.values.get時,必定會回傳range,但其它方法則不一定會有range。
同樣的,spreadsheets.values.get預設FORMATTED_VALUE,所以values會被限縮為string;但如果額外要求UNFORMATTED_VALUE,values還有可能是number、boolean或null。
總之,由於我不是真的很熟悉TypeScript,為了個人實作方便,這裡把interface限縮於我這次實作目標的型別。
上述是成功時會回傳的;由於原生JavaScript直至2025年的Error還是很陽春,這裡還要自己準備一個HTTP Error:
class GoogleSheetsError extends Error {
constructor(
// parent parameters
message: string,
// subclass properties
public readonly status?: number,
public readonly statusText?: string
) {
super(message);
this.name = "GoogleSheetsError";
}
}
雖然印象中有現成的社群套件提供各種各樣的Error,由於這裡我只需要一個簡單的,並且我個人很討厭追加套件,所以僅讓GenAI幫我生成HTTP Error後再微調。
在 《實作回顧:doGet(下)》,事先於GAS Web App環境設計好資料處理邏輯,只需要call一次就可以拿到所需資料。
而這次實作,則改在本地處理資料,所以必須多次call Goolge Sheets API。
為了方便我重複fetch,讀完官方文件,確定會用到的參數後,寫一個helper function:
async function fetchSpecificSheetData(
accessToken: string,
spreadsheetId: string,
range: string,
majorDimension: "ROWS" | "COLUMNS"
): Promise<GoogleSheetsResponse> {
const apiUrl: string = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}?majorDimension=${majorDimension}`;
try {
const response: Response = await fetch(apiUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new GoogleSheetsError(
`Google Sheets API error: ${response.status} ${response.statusText}`,
response.status,
response.statusText
);
}
const data: GoogleSheetsResponse = await response.json();
return data;
} catch (error) {
if (error instanceof GoogleSheetsError) {
throw error;
}
if (error instanceof Error) {
throw new GoogleSheetsError(
`Failed to fetch sheet data: ${error.message}`
);
}
throw new GoogleSheetsError(
"Unknown error occurred while fetching sheet data"
);
}
}
這樣一來,我在main function填入參數,就可以多次使用spreadsheets.values.get:
const toolstackLinkData: GoogleSheetsResponse = await fetchSpecificSheetData(
accessToken,
SPREADSHEET_ID,
"party-tools",
"COLUMNS"
);
以下是我當時練習的腳本的pseudocode:
=== Setup ===
- Define constants:
SPREADSHEET_ID = "<Google Sheet ID>"
OUTPUT_DIR = "./src/contentData/fetched/"
- Load your sacred GCP token from "./.env/gcp-token.json" 🔑
=== Helper Functions ===
createOutputPath(fileName):
→ build "<OUTPUT_DIR>/<fileName>.json" like a civilized dev
fetchSheetData(token, id, range, dim):
→ politely ask Google Sheets API for data
→ return JSON (if Google says yes)
transformDataToJson(columns, id):
→ turn ['name', 'Acme'] into { name: 'Acme' }
→ keep arrays if there’s a party of values 🎉
→ add { id } on top if given
→ return JSON object with good vibes
=== Main Flow ===
main():
token = getAccessToken()
key = fetchSheetData(token, SPREADSHEET_ID, "query!A2", ROWS)
if key:
partyData = fetchSheetData(token, SPREADSHEET_ID, `party-${key}`, COLUMNS)
save(transformDataToJson(partyData, key), "party.json")
toolstackData = fetchSheetData(token, SPREADSHEET_ID, "toolstack", ROWS)
save(transformDataToJson(toolstackData), "toolstackLink.json")
=== Entry Point ===
if run directly:
main()
如果不想像我一樣自己封裝fetcher,其實官方有提供諸如Python、Node.js等等封裝好的套件,比我自己寫的腳本代為處理了更多細節。
我自己沒有選擇用googleapis套件,主要是因為我當時覺得我所應用的場景很單純,用不到太複雜的機制,其次是我想了解一下基本的處理過程。使用套件長期來看,例如未來如果Google Sheets API大改版,維護上可能比較方便。
相比doGet,我必須在不同專案目錄維護doGet與fetcher兩組腳本,使用Google Sheets API則可以集中在同一個專案目錄管理所有邏輯,而且不用重複部署GAS Web App,所以顯然Google Sheets API比GAS HTTP endpoint更符合我的需求。
對我個人而言是小小的里程碑,從此以後,我就可以更有效率地sync資料。工作上會非常大量處理試算表,比起firebase,就我自己的工作場景,Google Sheets API已經堪用,且能很好地跟團隊內一般使用者所慣用的工具配合。
最後分享一個,我自己常常浪費時間、把腳本的log訊息排版成我覺得爽的格式😆:
Saved beach token to ./.env/deno-coconut.json
✓ Ocean access granted 🌴🌊
Fetching community id...
✓ Community id retrieved: "deno-beach-club" 🦕🏖️
Fetching full data from deno-beach-club sheet...
✓ Community sheet data fetched 🍹🌺
Transforming data into JSON object...
✓ Transformed JSON object:
{
"id": "deno-beach-club",
"name": "Deno 海灘俱樂部",
"projects": [
"sunset-jam-session",
"yakitori-drum-circle",
"tempura-surf-bot"
],
"toolstackLink": "vertical_tools_tropical"
}
✓ Community data saved to ./src/contentData/fetched/community.json 🏝️
Fetching data from TOOLSTACK_LINK sheet...
✓ TOOLSTACK_LINK sheet data fetched 🌈
Transforming data into JSON object...
✓ Transformed JSON object:
{
"general_tools": "https://beach.party/gen",
"dev_tools": "https://beach.party/dev",
"vertical_tools_tropical": "https://beach.party/tropical"
}
✓ ToolstackLink data saved to ./src/contentData/fetched/toolstackLink.json 🏄♂️
[^1]:最近跟在菲律賓主修CS的大學生閒聊到,他經歷過自己讀文件和後GenAI時期,他現在都傾向讓GenAI生成就好。我自己則是,以Deno為例,找到整理好的llms.txt,用穩定性比較高的 NotebookLM查找。希望未來每個官方文件網站都可以提供內建的、基於LLM的搜尋服務。
[^2]:當時實際上是讓GenAI依官方文件生成interface,我再進一步依我的實作目標做限縮。