如果你常在 Google 文件裡重複貼章節、改標題、加表格,其實不用手動來回切換;用 Apps Script 就能一鍵完成。
這篇文章要帶你認識 DocumentApp.getActiveDocument()——它就像伸手就能抓到「正在編輯的這份文件」,接著你能插入段落、建立目錄、調整樣式,甚至在開檔時自動彈出自訂選單。
內容不只講觀念,還會用清楚的步驟示範:怎麼建立容器綁定專案、首次授權要注意什麼、onOpen() 要怎麼配合,以及什麼情境該改用 openById()。最後再整理常見錯誤與踩雷清單(像是沒有 Active 文件、配額與效能問題),讓你少繞路、腳本一次就跑順。希望本篇文章可以幫助到需要的您。
目錄
{tocify} $title={目錄}
getActiveDocument() 是什麼?什麼時候該用?
DocumentApp.getActiveDocument() 會回傳目前這支腳本所「綁定(container-bound)」的 Google 文件,也就是你正在畫面上開啟、並且將 Apps Script 綁進去的那份 Doc。取得這個 Document 物件之後,你就能進一步改標題、插入段落/表格、套 Style、加頁首頁尾、建立自訂選單等。
官方文件明確說明:如果你要操作不是綁定的那份文件,請改用 openById() 或 openByUrl()。此外,getActiveDocument() 需要文件相關的授權範圍(Scopes)。
什麼情境該用?
你已經在 Google Docs 頁面中,透過「擴充功能 → Apps Script」打開編輯器,且腳本是綁定在這份 Doc 上。
你需要在這份「當前文件」加選單、加入按鈕、插入範例章節、格式化整份文件等。
什麼情境不該用?
定時器(Time-driven)或 Web App 這種沒有使用者互動、沒有打開任何 Doc 的背景環境;此時沒有「Active」的概念,請改用 openById() / openByUrl()。
你要批次處理多份文件(例如全公司周報),那就用 DriveApp 找到檔案 ID,再 openById() 逐一處理比較穩。
最安全的上手步驟
步驟 1:建立一份測試用 Google 文件
開 Google Docs,新建空白文件,隨便打幾行文字。
步驟 2:把 Apps Script 綁到這份文件
在該文件中按「擴充功能 → Apps Script」,會自動建立一個容器綁定(container-bound)的專案。
這種專案天然就能用 getActiveDocument() 取得你當前開著的這份 Doc。
步驟 3:貼入最小可運作範例
function helloDoc() {
const doc = DocumentApp.getActiveDocument(); // 取得「當前」這份 Doc
const body = doc.getBody();
body.appendParagraph('Hello, Google Docs!');
doc.saveAndClose(); // 儲存+關閉(可選)
}
步驟 4:首次執行與授權
在 Apps Script 編輯器按「執行」,系統會跳授權流程,允許存取文件。
getActiveDocument() 會用到文件讀寫相關 Scopes(documents.currentonly 或 documents)。
步驟 5:在文件中加自訂選單
function onOpen(e) {
DocumentApp.getUi()
.createMenu('自動化')
.addItem('插入範例章節', 'insertSampleSection')
.addToUi();
}
function insertSampleSection() {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
body.appendParagraph('專案總覽').setHeading(DocumentApp.ParagraphHeading.HEADING1);
body.appendParagraph('背景說明').setHeading(DocumentApp.ParagraphHeading.HEADING2);
body.appendParagraph('這裡寫你的內容...');
}
getUi() 僅能在綁定到文件的腳本內與目前開啟的文件互動,這點官方也有寫。
核心觀念拆解:Active、Bound 與非綁定
Active(目前):
指的是使用者螢幕上正在操作的那份 Doc。
Container-Bound(容器綁定):
腳本存在該 Doc 內部。「擴充功能 → Apps Script」打開的就是這種。
Standalone(獨立專案):
在 script.google.com 新建的腳本,不綁任何文件。這種腳本沒有「目前文件」可取用,執行 getActiveDocument() 不符合語境,請改 openById() / openByUrl()。
常用操作範例
標題、作者資訊、頁首頁尾
function setupDocMeta() {
const doc = DocumentApp.getActiveDocument();
doc.setName('專案企劃書 - v1'); // 檔名(實際是 Drive 上檔名)
const body = doc.getBody();
// 頁首頁尾(若沒有會先建立)
const header = doc.getHeader() || doc.addHeader();
header.clear();
header.appendParagraph('公司內部文件 | 機密');
const footer = doc.getFooter() || doc.addFooter();
footer.clear();
footer.appendParagraph('版權所有 © ' + (new Date()).getFullYear());
}
章節骨架生成(Heading + 內文)
function buildOutline() {
const body = DocumentApp.getActiveDocument().getBody();
const sections = [
{h: '摘要', level: 'HEADING1'},
{h: '市場分析', level: 'HEADING1'},
{h: '產品定位', level: 'HEADING1'},
{h: '時程與資源', level: 'HEADING1'},
{h: '風險評估', level: 'HEADING1'}
];
sections.forEach(s => body.appendParagraph(s.h).setHeading(DocumentApp.ParagraphHeading[s.level]));
body.appendPageBreak();
}
插入表格+套用表頭樣式
function insertKPI() {
const body = DocumentApp.getActiveDocument().getBody();
const table = body.appendTable([
['KPI', '目標', '目前值', '負責人'],
['月活用戶', '50,000', '18,200', '行銷'],
['轉換率', '5%', '2.3%', '產品'],
]);
// 表頭加粗
table.getRow(0).editAsText().setBold(true);
// 欄寬、對齊等可視需求再調
}
批次格式化(避免逐字 loop 的效能坑)
function emphasizeBody() {
const text = DocumentApp.getActiveDocument().getBody().editAsText();
// 一次性設定全文字體與大小,避免逐段/逐字慢速操作
text.setFontFamily(0, text.getText().length - 1, 'Noto Sans TC');
text.setFontSize(0, text.getText().length - 1, 12);
}
跨文件操作(不用 Active,用 openById)
function updateById(docId) {
const doc = DocumentApp.openById(docId); // 適合批次腳本與觸發器
const body = doc.getBody();
body.insertParagraph(0, '【系統批次更新】' + new Date());
doc.saveAndClose();
}
對於非當前文件或在背景環境(如定時器)執行時,請一律用 openById() / openByUrl()。
觸發器與 getActiveDocument():能不能一起用?
1. 簡單觸發器(Simple triggers,如 onOpen(e)):
在文件綁定腳本中非常好用,常用來加自訂選單或在文件打開時做初始化。這些觸發器有嚴格限制與配額,若動作涉及需要授權的服務(第一次),得先由使用者授權。
2. 定時器(Time-driven)/ 外部執行(Web App、API):
這類背景環境沒有 Active 文件,所以 不要 用 getActiveDocument(),直接 openById()。
3. 在試算表觸發器裡調 Document:
例如 onEdit(Sheets)去改 Docs,簡單觸發器通常不能跨存取其他檔案(需要授權)。解法是改用可安裝觸發器(Installable trigger),或改從獨立專案中用 openById()。
授權(Scopes)與權限要點
getActiveDocument() 需要文件相關 Scopes:https://www.googleapis.com/auth/documents.currentonly 或 https://www.googleapis.com/auth/documents(依你後續操作而定)。首次執行會跳授權視窗。
你對文件的實際權限(檢視/留言/編輯)也會影響操作成功與否:例如你只有檢視權,卻要改內容,一定報錯。
配額與效能(避免踩限制造成中斷)
Apps Script 各服務都有每日配額與執行時間限制;文件服務(DocumentApp)的呼叫也算在內。若超標會拋出例外,官方提供「配額總表」與「檢視/編輯配額」頁面可參考。一般常見還有每次執行的時限、觸發器總時數等。
降風險建議:
減少呼叫次數:
能一次性設定文字樣式就不要逐字 loop(見上方範例)。
批次處理分段跑:
大量文件請分批、多個執行;必要時在兩批之間留 Utilities.sleep()。
例外處理與重試:
以 try/catch 包住關鍵操作,必要時實作簡單的退避(backoff)策略。
避免不必要的 saveAndClose():
頻繁呼叫會放大效能消耗。
非官方社群文常會提到文件服務每日操作次數的「經驗值」或 Workspace 與個人帳的差異,但實際數字請以 Google 官方配額頁面為準,因為會隨方案/專案而變更。
錯誤處理與偵錯技巧
常見錯誤訊息與對策
1. TypeError: Cannot read properties of null/undefined ...
多半是 DocumentApp.getActiveDocument() 回來是 null(沒有 Active 文件的環境),或是你串到不存在的元素。確認是否在綁定文件內執行;若在定時器/Web App,請改 openById()。
2. 權限/授權相關錯誤
第一次執行沒授權就被觸發器叫起來;或你只有檢視權卻要寫入。請先用手動執行跑一次授權流程,並檢查文件分享權限。
3. 配額超限(Service invoked too many times / Exceeded maximum execution time)
表示你呼叫太密集或單次執行超時。請參考上一節的降風險建議,並檢查官方配額文件。
偵錯小技巧
Logger.log 與 執行記錄:逐步印出流程與關鍵變數。
最小可重現:從最短的操作開始排查(例如只做 getActiveDocument() + appendParagraph())。
手動先跑一次:讓 Scopes 就緒,避免簡單觸發器第一次就撞牆。
實務範例
範例 A:一鍵產生企劃書模板
function createProposalTemplate() {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
body.clear(); // 先清空(請小心使用)
body.appendParagraph('產品企劃書').setHeading(DocumentApp.ParagraphHeading.TITLE);
const sections = [
'一、專案背景', '二、目標與指標', '三、使用者洞察',
'四、功能規劃', '五、時程與資源', '六、風險控管'
];
sections.forEach((h) => body.appendParagraph(h).setHeading(DocumentApp.ParagraphHeading.HEADING1));
const kpiTable = body.appendTable([
['KPI', '目標', '目前值', '註記'],
['月活', '50,000', '-', ''],
['留存 D30', '20%', '-', '']
]);
kpiTable.getRow(0).editAsText().setBold(true);
}
範例 B:把文件內容搬到另一份 Doc(使用 openById)
function cloneTo(docIdTarget) {
const source = DocumentApp.getActiveDocument().getBody();
const target = DocumentApp.openById(docIdTarget).getBody();
// 簡化示範:把純文字搬過去,保留基本段落(複雜格式請個別處理)
target.appendParagraph('【來自原文件】');
target.appendParagraph(source.editAsText().getText());
}
跨文件處理時,直接對目標文件使用 openById() 最穩。遇到複雜元素(表格、清單、圖片)建議逐一複製或以模板法處理。
範例 C:打開文件就出現自訂選單(Simple Trigger)
function onOpen(e) {
DocumentApp.getUi()
.createMenu('報告工具')
.addItem('插入封面', 'insertCover')
.addItem('加目錄', 'addTOC')
.addToUi();
}
function insertCover() {
const body = DocumentApp.getActiveDocument().getBody();
body.insertParagraph(0, '專案報告').setHeading(DocumentApp.ParagraphHeading.TITLE);
}
function addTOC() {
const body = DocumentApp.getActiveDocument().getBody();
body.insertParagraph(1, '').asParagraph(); // 空行
body.insertTable(2, [[DocumentApp.ElementType.TABLE_OF_CONTENTS]]);
}
onOpen(e) 屬於簡單觸發器,規則與限制請參考官方「Triggers」指南。
常見問題與雷點
1. 在非綁定環境呼叫 getActiveDocument()
例如獨立專案、定時器、Web App。結果不是 null 就是報錯。解法:改 openById() / openByUrl(),不要依賴 Active。
2. 把 Sheets 的 onEdit(e) 當萬能用
簡單觸發器(如 onEdit)無法直接跨檔案做需要授權的動作。解法:用可安裝觸發器或改由獨立腳本執行,並使用檔案 ID 操作。
3. 第一次沒授權就讓觸發器跑
會因缺乏授權而失敗。解法:先手動跑一次,完成 Scopes 授權。
4. 過度逐字/逐元素操作,效能雪崩
文件 API 操作屬於高成本呼叫。解法:偏向「整段處理、一次設定」,減少呼叫次數;大量處理時分批執行,搭配 try/catch 與簡單 backoff。
5. 把非官方數字當鐵律
社群文章的「每日 N 次」僅供參考。解法:以官方配額頁與實際專案配額為準。
6. 誤以為 getActiveDocument() 等同「目前視窗的任何文件」
它回的是綁定那份文件,不是你瀏覽器任何開啟的文件。解法:需要指定其他文件時,用 openById()。
實務最佳做法
明確選擇模式:當前文件就 getActiveDocument();批次與觸發器就 openById()。
先手動授權:跑一次主流程,讓 Scopes 就緒。
控制呼叫次數:能一次性設定就不要逐字 loop。
錯誤處理:try/catch,必要時加退避重試。
分批執行:大量文件拆批處理,監控執行記錄。
自訂選單入口:讓非技術同事也能一鍵操作(onOpen + getUi())。
文件權限檢查:沒有編輯權就不要嘗試寫入。
配額監控:熟悉官方「服務配額」與「檢視/編輯配額」文件。
問題集
Q1:我在獨立專案呼叫 getActiveDocument(),為何是 null?
A:因為沒有「容器綁定」的 Active 文件。請改 openById()。
Q2:想用排程自動生成報告,可以用 getActiveDocument() 嗎?
A:不行。排程沒有 Active 文件。請以 ID 開檔並處理。
Q3:第一次裝腳本就讓 onOpen(e) 自動跑,結果報授權錯?
A:先手動執行一次任一需要授權的函式,完成授權,之後觸發器才會順利。
Q4:我要對好多文件跑相同流程,怎麼兼顧效能與配額?
A:用 DriveApp 搜 ID → openById() → 批次跑,控制每次處理量、減少 API 呼叫、加上錯誤重試並監控配額。
