Google Apps Script でよく使う機能を関数としてまとめてみた
Google Apps Script (GAS) は面倒な作業の自動化や、スプレッドシートの簡易データベース化などの様々な場面で世話になるプラットフォームです。GAS では V8 ランタイムが使えるため、フロントエンドエンジニアは JavaScript と同じように GAS を書くことができます。ここでは GAS でよく使う機能を関数としてまとめました。
目次
GET リクエストを受け取る
GET リクエストを受け取るのは doGet
という決まった関数名になります。
/**
* @see https://developers.google.com/apps-script/guides/web
*/
function doGet(e) {
try {
const { queryString, parameter, parameters, postData } = e;
const queries = [...new URLSearchParams(queryString).entries()].reduce((obj, e) => ({...obj, [e[0]]: e[1]}), {});
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ error, e })).setMimeType(ContentService.MimeType.JSON);
}
}
POST リクエストを受け取る
POST リクエストを受け取るのは doPost
という決まった関数名になります。
/**
* @see https://developers.google.com/apps-script/guides/web
*/
function doPost(e) {
try {
const { queryString, parameter, parameters, postData } = e;
const { length, type, contents, name } = postData;
const json = JSON.parse(contents);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ error, e })).setMimeType(ContentService.MimeType.JSON);
}
}
Google App Scripts で CORS エラーが発生した場合の対応策
GAS で
Access to fetch at ‘https://script.google.com/macros/s/XXXXXXX/exec‘ from origin ‘https://example.com‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
のような CORS エラーに遭遇することがあります。これには以下の2つの原因が考えられます。
- 単純リクエストではなくプリフライトリクエストを送信してしまっている
- GAS のコード自体でエラーが発生している
それぞれ詳しく説明していきます。
単純リクエストではなくプリフライトリクエストを送信してしまっている場合
MDN の CORS に関するガイドで単純リクエストと呼ばれるリクエストがあり、このリクエストを送っている限りは CORS プリフライトを発生させません。単純リクエストというのは5つの条件を満たすもので、そのうち特に重要なのが以下の3つの条件です:
GET
/HEAD
/POST
のどれかのメソッドである- ユーザーエージェントによって自動的に設定されたものを除いて、
Accept
/Accept-Language
/Content-Language
/Content-Type
以外のヘッダーが設定されていない Content-Type
がapplication/x-www-form-urlencoded
、multipart/form-data
、text/plain
のうちのどれかになっている
残りの条件については上記 MDN のサイトでご確認ください。
3番目は重要で、例えば GAS と JSON でやりとりしたいからといって appication/json
にしてしまうとプリフライトリクエストが送られてしまうため、CORS エラーになってしまいます。対処法としては、例えば Fetch API の場合は
const response = await fetch(url, { method: 'POST', body: JSON.stringify(payload) });
のように JSON.stringify
した文字列を、デフォルトの Content-Type: text/plain;charset=UTF-8
で送信すれば、CORS プリフライトは発生しません。GAS の受け側では
function doPost(e) {
const json = JSON.parse(e.postData.contents);
}
のようにして JSON 文字列をパースします。
サイトによっては fetch
するときに mode: 'no-cors'
を指定すれば大丈夫、ということが書かれていることがありますが、モードを no-cors
にすると fetch
の戻り値からレスポンスを取得することができなくなります。レスポンスを取得しなくてもいい場合はそれでも問題ありませんが、多くの場合は GAS からのレスポンスも拾いたいため、mode: 'no-cors'
の指定では解決になりません。
GAS のコード自体でエラーが発生している場合
GAS では、例えば POST リクエストを
https://script.google.com/macros/s/XXXXXXX/exec
に送信してリクエストが問題なく処理されると、そこから直接レスポンスが返ってくるのではなく、302 リダイレクトされて
https://script.googleusercontent.com/macros/echo?user_content_key=...
からレスポンスが送られてきます。このとき script.googleusercontent.com
から返されるレスポンスには access-control-allow-origin: *
が含まれています。
一方、もし GAS のコード自体で何かしらのエラーが発生した場合は
https://script.google.com/macros/s/XXXXXXX/exec
からそのままステータスコード 200 でレスポンスが返ってきます。このときレスポンスには access-control-allow-origin
は含まれておらず、ブラウザ側では CORS エラーとして処理され、コンソールに
Access to fetch at ‘https://script.google.com/macros/s/XXXXXXX/exec‘ from origin ‘https://example.com‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
といったエラーが表示されるようになります。
これは doGet
や doPost
の中をすべて try ~ catch
で囲んで、GAS 側でのエラーを外に漏らさずに GAS 内で捕縛するようにすることで対処できます。
function doPost(e) {
try {
const { queryString, parameter, parameters, postData } = e;
const { length, type, contents, name } = postData;
const json = JSON.parse(contents);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ error, e })).setMimeType(ContentService.MimeType.JSON);
}
}
Google Forms の送信イベントを受け取る
Google Forms では、GAS でフォームをイベントのソースとすることで、フォーム送信後に自動返信メールを送信したりすることができます。フォームイベントのレスポンス FormResponse
では、フォームを送信したユーザーのメールアドレスやフォームの回答項目を取得することができます。
function onFormSubmit(e) {
// 回答ID、タイムスタンプ、メールアドレスは以下のようにして取得します.
const id = e.response.getId();
const timestamp = e.response.getTimestamp();
const email = e.response.getRespondentEmail();
const itemResponses = e.response.getItemResponses();
// デバッグ用に回答項目のログを出力.
itemResponses.map((itemResponse, i) =>
Logger.log('Response #%s to the question "%s" was "%s"',
(i + 1).toString(),
itemResponse.getItem().getTitle(),
itemResponse.getResponse()
)
);
// フォームの項目が3つある場合は以下のようにして回答内容を取得できる.
const [value1, value2, value3] = itemResponses.map(itemResponse => itemResponse.getResponse());
}
HTML メールを送信する
HTML メールの送信には HtmlService
の createTemplateFromFile
を利用します。このメソッドに HTML ファイル名を渡し、同名のファイルを作成してそこに HTML メールの内容を記述します。また HtmlTemplate
オブジェクトにプロパティを生やすことで、HTML メールのテンプレートに GAS の変数の値を渡すことができます。
// main.gs
function onFormSubmit(e) {
const to = 'user@example.com';
const from = 'info@example.com';
const template = HtmlService.createTemplateFromFile('message');
template.to = to;
const htmlBody = template.evaluate().getContent();
const subject = 'お問合せいただきありがとうございます';
const body = htmlBody.replace(/<[^>]*>?/gm, '');
const options = { from, htmlBody };
GmailApp.sendEmail(to, subject, body, options);
}
HTMLメールのテンプレートとなる message.html
は以下のようになります。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p><?=email?> 様</p>
<p>いつも大変お世話になっております。<br>◯◯◯◯カスタマーサポートでございます。</p>
<p>この度はお忙しい中アンケートにご協力いただきまして、<br>誠にありがとうございました。</p>
<p>いただきましたご意見、ご要望を、<br>今後のサービスや業務改善に役立てて参ります。</p>
<p>今後ともご愛顧賜りますよう、よろしくお願い申し上げます。</p>
</body>
</html>
Spreadsheet のシートを作成/選択する
sheetName
という名前のシートがあればそのシートを、なければその名前でシートを作成する関数はこのようになります。
function getSpreadSheet(sheetName) {
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName(sheetName)
if (sheet) return sheet;
return ss.insertSheet(`${sheetName}`)
}
関数の呼び出し元では
const sheet = getSpreadSheet('シート名1');
if (sheet.getDataRange().isBlank()) sheet.appendRow(['ヘッダー1', 'ヘッダー2', 'ヘッダー3']);
のように使うことができます。新規作成された場合はシートが空のため、値が存在するセルの範囲を返す getDataRange
が空になります。そのためここでは sheet.getDataRange().isBlank()
で空のシートかどうかを判定してヘッダーを挿入する、といった操作をおこなっています。
Spreadsheet のシートの重複を削除してソートする
GAS ではシートの中でデータが存在する Range
を取得するために Range.getDataRange
、取得した Range
についてソートするために Range.sort
、重複した行を削除するために Range.removeDuplicates
というメソッドがそれぞれ用意されています。
また SpreadsheetApp.getActive()
は Spreadsheet
クラスのオブジェクト、Spreadsheet.getSheetByName()
は Sheet
クラスのオブジェクト、Sheet.getDataRange
は Range
クラスのオブジェクト、Range.sort
と Range.removeDuplicates
は Range
クラスのオブジェクトを返すため、以下のようなメソッドチェーンで書くことができます。
function sortAndRemoveDuplicates(sheetName) {
SpreadsheetApp
.getActive()
.getSheetByName(sheetName)
.getDataRange()
.sort({ column: 1, ascending: false })
.removeDuplicates([1]);
}
XML ファイルをパースする
外部リソースの取得は UrlFetchApp
、XML のパースは XmlService
を利用します。
function parseXml(url) {
const xml = UrlFetchApp.fetch(url).getContentText();
const document = XmlService.parse(xml);
const root = document.getRootElement();
const channel = root.getChild('channel');
const entries = channel.getChildren('item');
return entries.map(entry => ({
title: entry.getChild('title').getText(),
link: entry.getChild('link').getText(),
pubDate: entry.getChild('pubDate').getText(),
guid: entry.getChild('guid').getText(),
categories: entry.getChildren('category').map(category => category.getText());
}));
}