2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoogleAppsScript チーム開発のためのアイデア集

Posted at

こんにちは、luthです

GASからプログラミングに入門し、Vue/React、Typescriptを勉強していったノンプログラマーですが、チーム内で利用する、社内ツール開発を4年ほどやってきました

生成AIで簡単にコードを出せる世の中になり、ノンプロでも用意にGASツールを利用できるようになりましたが、仕事で利用するなら確実性・継続性を持ちたいものです

そのため、個人開発の中で感じた、以下のためのポイントをまとめてみました

  • 非プログラマーチームでGAS開発をするための工夫
  • 個人開発では意識しない、チームならではのポイント
  • 保守性・可読性の確保

JavaScriptやTypeScriptのチーム開発を経験された方の多くにとっては既知の内容かと思いますが、
GASは初学者や非開発経験者、個人開発者も比較的多いかと思いますので、活用いただければと…!

*チーム利用ツールのためのアイデア、こちらにまとめています

想定読者

  • 社内やチーム内で共用する、GASツールの開発者
  • GAS 初心者〜上級者
  • ブラウザのGASエディタで開発する環境
    = Node環境を入れない・入れられない(Clasp が利用できない)環境

上記の通り、@google/clasp@google/asideを利用したローカル開発については基本的に触れません

チーム開発でのポイント

開発体験の向上のためのポイント。GASならではの注意点を中心に記載します。


コード規約をざっくりでも決めておく

コード規約とは「このチームでは、こういうルールでコードを書きましょう」という決めです。
特に、GASエディタは内蔵のフォーマッタ(Windows: Alt + Shift + F)がありますが、フォーマットルールは変えられませんし、Node.jsのPrettierと比べれば、かなり緩い印象です。

参考となるサイトをいくつかご紹介。


開発環境と本番環境を分ける

GAS開発をしていると、開発規模の小ささや管理環境構築の難しさから、git管理をせず、環境も本番環境だけ、ということになりがちです。。。
ただ、GASのようなスケール感でもビジネスに利用していることから、多少の管理コストを抱えてでも、「新規/追加開発」や「バグ修正」のための開発環境を用意すべきです。

具体的には、

  • コンテナバインドなら、スプレッドシートやフォームなど、紐づいているファイルごとコピー
  • スタンドアロンなら、GASファイル自体をコピー
  • BotやDBなど、関係するファイル/設定も、開発用と本番用を分ける

こうすることで、以下のような効果を見込めます

  • 開発中・テスト中に、本番用の動作に影響を及ぼさない
  • 複数人で、別の機能を開発して、本番環境で統合する(branch的な考え)
  • 開発環境で不可逆的なミスを犯した場合、本番環境をコピーすることで復元を図れる(バックアップ的な考え)

ローカル開発ができる環境の方は、@google/asideを利用すると、
開発用GASプロジェクト・本番用GASプロジェクトを分けて設定し、
@google/claspを利用したプッシュ・デプロイができます…!
ご活用ください~


プロジェクト設定は最初に確認

ランタイム(JavaScriptのバージョン指定)や、タイムゾーン(実行中に、どの都市の時間として実行するか。Dateを利用する際に影響がある)などを最初に確認するとよい。

ランタイム:V8
タイムゾーン:(GMT+09:00) 日本標準時 - 東京(Asia/Tokyo)


変更履歴はすこしやっかい

GoogleWorkspace系は変更履歴が手厚いサービスが多いですが、GASは旧エディタ時代にはあったものの、新エディタになって久しく変更履歴機能はありませんでした。

2023/09に変更履歴機能が再び実装されましたが、少しクセがあります。。

  • 見れるのはデプロイバージョン間の比較のみ
  • 比較はdiff(バージョン間よの差分)

GASは通常。Webアプリ、ライブラリ、AddOnアプリなどとして開発しない限りはデプロイは不要なため、あまり有効ではないdiffな気がします…

Githubが使える環境の方は、Chrome拡張機能のこちらでぜひ。
Google Apps Script Github アシスタント

Githubもclaspも使えない…という方には、こんな方法をご提案
GASで、別ファイルのGASファイルの中身を取得して、スプレッドシートやらドライブやらで管理する方法。
GASでGASプロジェクトを取得


ドキュメンテーションコメント(jsDoc)をできるだけ書く

チームで開発する際は、全員が全員、すべてのコードを読むとは限りません。
特に関数やグローバル定数については使いまわしを想定するかと思いますので、以下などを記述してあげると優しいです。

  • どういった意図の定義なのか
  • 引数や返り値の型、内容
  • エラーが起きる条件

データ型を記述してあげると、エディタ上の補完に活用されますので、
バグ対策にもなりますね…!

modules.gs
/**
 * マスターシートのシート名
 */
const MASTER_SHEET_NAME = 'これがマスターシート';

/**
 * 指定したシート名の情報を取得する
 * @error シート名が存在しない場合、もしくは取得開始行番号が1未満の場合、エラー
 * @param {string} sheetName 取得したいシート名
 * @param {number} [startRow] 取得を開始したい行番号(1始まり) 。デフォルト:1
 * @returns {{ index: number, id: string, title: string, date: Date, sheet: SpreadsheetApp.Sheet }[]} 取得結果
 * - index: 行番号(1始まり)
 * - id: A列のID
 * - title: B列のタイトル
 * - date: C列の入力日
 * - sheet: シートオブジェクト
 */
function getTable(sheetName, startRow = 1) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);
  if (!sheet)
    throw Error(`シート名「${sheetName}」のシートが取得できませんでした`);
  if (startRow == null || startRow < 1)
    throw Error(`取得開始行番号「${startRow}」が不正です`);
  const cells = sheet
    .getRange(startRow, 1, sheet.getLastRow(), sheet.getLastColumn())
    .getValues();
  return cells.map((row, index) => {
    const [id, title, date] = row;
    return {
      index: index + startRow,
      id,
      title,
      date: new Date(date),
      sheet,
    };
  });
}

jsDoc用の型定義もどきも可能

GASは、JavaScriptベースなので静的な型定義はできませんが、jsDocで型定義を記述でき、別の箇所のjsDocで使いまわせます。
少しでもエディタが推論できるように、jsDocを楽に記述できるように、定義してあげましょう。

user.gs
/**
 * ユーザー情報
 * @typedef {object} UserType
 * @prop {string} id
 * @prop {string} name
 * @prop {number} age
 * @prop {UserType[]} friends
 * @prop {string} address ユーザーのEmailアドレス
 * @prop {SpreadsheetApp.Sheet} mySheet ユーザーごとに生成されるシート
 */

/**
 * ユーザー情報取得
 * @param void
 * @returns { UserType[] } 取得したユーザー情報
 */
function getUsers() { ... }
main.gs
function noticeUsers() {
  const users = getUsers();
  // GASエディタ上で入力補完が効く
  users. // forEach, map, filterなど…
  users[0]. // id, name, ageなど…
  users[0].mySheet. // getRange, setName, getLastRowなど…

}

// `@type`で変数・定数に型をつけることも可能
/** @type { UserType } */
const user = {
  id: 'd023jg',
  name: 'Test user',
  // ...
};

共通利用できる処理はライブラリ化

業務特性でよく使う処理などは、何度も同じコードを記述するのではなく、
関数としてラッピングし、再利用できるようにすべきです。

GASは「ライブラリ」という仕組みで、ほかのプロジェクト(GASファイル)の関数を呼び出して利用することができるので、
複数のプロジェクトで、同じ処理を2度記述することなく、効率的に開発することができます。

ライブラリ側プロジェクト「TeamLib」- code.gs
// 紐づいているスプレッドシートのシート「TABLE」から、チームメンバーの情報を取得する処理
// 別関数で、実行者の名前を処理に入れる、実行者に応じて処理を分岐させるための分岐要素を入れる、など可能

/**
 * メンバー情報取得
 * @param {string} address 対象メンバーのアドレス情報
 * @returns {object} メンバー情報
 * - {string} address アドレスの返却
 * - {string} name 名前(苗字のみ)
 * - {number} age 年齢
 * - {Date} lastUpdated 最終更新日
 */
function getMemberInfo(address) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('TABLE');
  const values = sheet
    .getDataRange()
    .getValues()
    .map((row) => {
      let [address, name, age, lastUpdated] = row;
      lastUpdated = new Date(lastUpdated);
      return { address, name, age, lastUpdated };
    });
  const member = values.find((row) => row.address === address);
  if (!member)
    throw Error(`メンバー情報が見つかりませんでした。アドレス:${address}`);
  return member;
}

これをライブラリとしてデプロイして、別プロジェクトで「TeamLib」として読み込むと、簡単に同じ処理を使いまわせます!

利用側プロジェクト - main.gs
function main() {
  const me = Session.getActiveUser().getEmail();
  const memberInfo = Teamlib.getMemberInfo(me);
  console.log(`今の実行者は「${memberInfo.name}」さんです`);
  // => 今の実行者は「山口」さんです
}

GASのライブラリでは、一部のjsDocは反映されますが、
特にデータ型については完全には反映されません。
顕著に影響を受けるのはオブジェクトとクラスですね。

ライブラリでの「jsDoc」の不完全性
ライブラリ
// jsDocで返り値オブジェクトの各プロパティのデータ型を設定している場合

/**
 * データ取得関数
 * @returns {{ id: string, name: string, index: number }}
 */
function getData() {
  return {
    id: 'abcde',
    name: 'test',
    index: 0,
  };
}


// クラスを定義して、呼び出し用関数を定義している場合

class Animal {
  /**
   * @param {string} name
   */
  constructor(name) {
    this.name = name;
  }

  /**
   * ご挨拶
   * @returns {string}
   */
  hello() {
    return `Hello, ${this.name}`;
  }
}

/**
 * クラス呼び出し関数
 * @param {string} name
 * @returns {Animal}
 */
function getAnimal(name) {
  return new Animal(name);
}

上記でjsDocを定義すると、ライブラリ側のプロジェクト内だと入力補完が有効なのですが、
利用側のプロジェクトだとすべてobject扱いされ、中身が見えなくなります…

利用側
// ライブラリは`MyLib`として呼び出し設定

function main() {
  const data = MyLib.getData();
  data. // => 入力補完が効かず、`id`, `name`, `index`が候補にならない。。。

  const dog = MyLib.getAnimal('dog');
  dog. // => 入力補完が効かず、`name`, `hello()`が候補にならない。。。

}

ライブラリの作成方法や利用方法は、他記事をご参照ください。


トリガーは個人依存で、他アカウントのトリガーは変更できない

GASの中でも「トリガー」という機能がとても便利です…!
時間ベースで処理する(cron)機能や、紐づいているFormやSpreadsheetなどのイベントに合わせて、関数を自動で起動させるものです。

代表的なトリガーは以下のようなものです

  • 〇分ごとに処理
  • 毎日〇時台に処理
  • YYYY/MM/DD HH:mm に1度だけ処理
  • フォームが送信されたときに処理
  • スプレッドシートが開かれたときに処理

これらは非常に便利な機能ですが、チーム開発時に1点要注意なポイントが。

GASの基本思想が「個人に閉じたもの」な雰囲気があり、
トリガーも「自分で設定したトリガーは自分しか編集・削除できない」という性質があります。
また、トリガーで起動する関数の実行者は、トリガー設定者のアカウントになるので、
アカウントに依存する処理(GmailApp全般、Session.getActiveUser()、アクセス権全般…etc)はトリガー設定者目線での処理となります。

この「自分で設定したトリガーは自分しか編集・削除できない」という性質は、言い換えれば「他のメンバーが作成したトリガーは、作成者以外には編集・削除できない」ため、
アカウントに依存するような処理があり、トリガー管理者が異動・退職などをする場合は、前トリガーが残っていることで影響があるので、トリガー管理者の引継ぎ作業が必要となります。

人事異動のタイミングなどで、前管理者のトリガー削除・新管理者のトリガー作成を行うよう、手順書を作っておきましょう…!


以上となります!
書きなぐってしまいましたが、どれか一つでもGAS開発者にささって、開発の一助となれれば嬉しいです

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?