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つの原因が考えられます。

  1. 単純リクエストではなくプリフライトリクエストを送信してしまっている
  2. GAS のコード自体でエラーが発生している

それぞれ詳しく説明していきます。

単純リクエストではなくプリフライトリクエストを送信してしまっている場合

MDN の CORS に関するガイドで単純リクエストと呼ばれるリクエストがあり、このリクエストを送っている限りは CORS プリフライトを発生させません。単純リクエストというのは5つの条件を満たすもので、そのうち特に重要なのが以下の3つの条件です:

  • GET / HEAD / POST のどれかのメソッドである
  • ユーザーエージェントによって自動的に設定されたものを除いて、Accept /Accept-Language / Content-Language / Content-Type 以外のヘッダーが設定されていない
  • Content-Typeapplication/x-www-form-urlencodedmultipart/form-datatext/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.

といったエラーが表示されるようになります。

これは doGetdoPost の中をすべて 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 メールの送信には HtmlServicecreateTemplateFromFile を利用します。このメソッドに 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.getDataRangeRange クラスのオブジェクト、Range.sortRange.removeDuplicatesRange クラスのオブジェクトを返すため、以下のようなメソッドチェーンで書くことができます。

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());
  }));
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です