Google Apps Scriptライブラリの作り方

# 1. はじめに
---

本記事では、Google Apps Script(GAS)のライブラリの作り方、JSDocコメントやコード補完まで、詳しく解説しています。

---

```
💡 この記事で得られる知識
・ ライブラリの作り方と公開方法
・ JSDocコメントの書き方
・ Google Apps Scriptのコード補完について
```

---

Google Apps Scriptでは作成したスクリプトをライブラリとして一般公開することができます。
もちろん組織内や特定ユーザーに限定することも可能です。
また、公開されているライブラリを自身のスクリプトで利用することもできます。

次よりライブラリの作り方から公開するまでの手順を解説していきます。

# 4. ライブラリの作り方

---

## 4-1. ライブラリを作る

ライブラリの作成手順を説明します。

---

1) 新しいスクリプトファイルを作成する
Googleドライブで新規に「Google Apps Script」ファイルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/aeda8dbb-7836-e6e7-c962-ecb284846ae4.png)

---

2) コードを書く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/b5607f73-fbcd-feb0-9e1c-884603355d2d.png)

---

3) 新しい版として保存する
メニュー > 「ファイル」 > 「版を管理」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/2ff096eb-a46d-cd65-c4fa-08ed1182bf51.png)

---
版（=バージョン）の説明を「Desribe what has changed...」に記載し、「Save new version」で新しい版として保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/fcfdb537-ad92-8511-17d4-613a59eb04d1.png)

---

4) 共有設定をする
全公開、全公開（リンクを知っているユーザーのみ）、組織内、組織内（リンクを知っているユーザーのみ）、特定のユーザーのみ、などの共有設定をします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/4c416256-c6bc-82c1-8737-1f604dbf5469.png)

---

5) スクリプトIDを公開する
3-1.に記載した手順にてスクリプトIDを参照し、公開します。
利用者はスクリプトIDさえ知っていれば良いので、ブログやQiita、GitHubなど、公開場所は好きな場所で構いません。

---

## 4-2. ライブラリ作成時の注意事項

ライブラリを作成する際に気を付ける点について説明します。
ECMAScriptより実装可能なクラスの利用、スクリプトエディタでのコード補完について解説します。

---

### 4-2-1. クラスの利用

ライブラリ内にクラスを自作した場合、「識別子.クラス名」としてもライブラリのクラスを参照することはできません。
下記のような結果になります。

```javascript:ライブラリ側（識別子=Lib）
class Foo {
  static hoge() {
    console.log('Foo#hoge()')
    return true
  }
}
```

```javascript:利用側
console.log(Lib.Foo) // undefined

const foo = new Lib.Foo() // エラー

console.log(Lib.Foo.hoge()) // エラー
```

---

これを解消するためには、以下のような方法があります。

1) クラスをsuper global領域にセットする
2) クラスを取得するsuper globalな関数を作成する
3) クラスのインスタンスを生成し取得するsuper globalな関数を作成する

---

📝 global
---
JavaScriptのランタイムではいくつかのオブジェクトを持っていますが、このオブジェクトはツリー構造になっています。
共通して出てくる「super global」という言葉ですが、ここで言う「super global」とはその最上位を指しています。

Google Apps Scriptではネストのない場所か、ネストのない場所に書いた「this」がsuper globalな階層に該当します。
ただし、ネストのない場所に書いたletやconstはsuper globalのひとつ下の階層(local globalと呼称します)に位置します。

---

以下の「宣言」と「階層の確認」のコードでそれが確認できます。

```javascript:宣言
this.a = 1
var b = 2
let c = 3
const d = 4
function e() {
  return 5
}
```

---

```javascript:階層の確認
function test() {
  console.log(this.a) // 1
  console.log(this.b) // 2
  console.log(this.c) // undefined
  console.log(this.d) // undefined
  console.log(this.e()) // 5
  
  console.log(a) // 1
  console.log(b) // 2
  console.log(c) // 3
  console.log(d) // 4
  console.log(e()) // 5
}

```

---

test()を実行すると、階層の確認の各行に書いたコメントの値が出力されます。

letで宣言したc、constで宣言したdはthisの直下には存在しませんが、test()内からは参照できる場所に位置していることがわかります。

つまり、以下のことが言えます。

- a, b, e()はsuper globalに位置している
- a, b, c, d, e()はlocal globalに位置している

---

尚、「super global」や「local global」は、あくまでも本記事内での説明のために使用している用語です。正確な用語があるかもしれません。

---

次よりクラスを利用する3つの方法について実際のコードを交えて説明していきます。

1) クラスをsuper global領域にセットする
2) クラスを取得するsuper globalな関数を作成する
3) クラスのインスタンスを生成し取得するsuper globalな関数を作成する

---

1) クラスをsuper global領域にセットする
thisのプロパティに対してクラスを代入します。

```javascript:ライブラリ側（識別子=Lib）
class Foo {
  constructor() {
    console.log('Foo#constructor()')
  }

  hoge() {
    console.log('Foo#hoge()')
    return true
  }
}

this.foo = Foo // クラスをglobal領域にセットする
```

```javascript:利用側
function myFunction() {
  const foo = new Lib.foo() // 「Foo#constructor()」が出力される
  foo.hoge() // 「Foo#hoge()」が出力される
}
```

---

2) クラスを取得するsuper globalな関数を作成する
クラスを取得するfunctionを作成します。

```javascript:ライブラリ側（識別子=Lib）
class Foo {
  constructor() {
    console.log('Foo#constructor()')
  }

  hoge() {
    console.log('Foo#hoge()')
    return true
  }
}

function foo() { return Foo } // クラスを取得する
```

```javascript:利用側①
function myFunction() {
  const foo = new (Lib.foo())() // 「Foo#constructor()」が出力される
  foo.hoge() // 「Foo#hoge()」が出力される
}
```

```javascript:利用側②
const Foo = Lib.foo() // クラスをlocal globalな定数に入れてしまう

function myFunction() {
  const foo = new Foo() // 「Foo#constructor()」が出力される
  foo.hoge() // 「Foo#hoge()」が出力される
}
```

---

3) クラスのインスタンスを生成し取得するsuper globalな関数を作成する
クラスのインスタンスを生成して返却するfunctionを作成します。

```javascript:ライブラリ側（識別子=Lib）
class Foo {
  constructor() {
    console.log('Foo#constructor()')
  }

  hoge() {
    console.log('Foo#hoge()')
    return true
  }
}

function foo() { return new Foo() } // クラスのインスタンスを生成して取得する
```

```javascript:利用側
const foo = Lib.foo() // 「Foo#constructor()」が出力される
foo.hoge() // 「Foo#hoge()」が出力される
```

---

## 4-3. JSDocとコード補完

---

### 4-3-1. JSDoc

JSDocの説明について、Wikipedia記事からの引用を拝借します。

> JSDocは、JavaScriptのソースコードにアノテーションを追加するために使われるマークアップ言語である。JSDocをコメントの中に含めることで、プログラマーは自分が書いたコードのAPIを記述するドキュメントを追加することができる。JSDocをさまざまなツールで処理することで、HTMLやRich Text Formatなどの形式のアクセス可能なドキュメンテーションを自動生成することができる。JSDocは、Apache License 2.0の元にライセンスされている自由ソフトウェアである。

引用元: [https://ja.wikipedia.org/wiki/JSDoc](https://ja.wikipedia.org/wiki/JSDoc)

---

Google Apps Scriptでサポートされているアノテーションは以下です。

| アノテーション | 概要 |
| --- | --- |
| ＠param | 引数の型、名称、説明 |
| ＠return | 戻り値の型、説明 |

※ [Libraries  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/guides/libraries) より引用

> Only the @param and @return annotations are currently supported.

---

JSDocですが、3-1.で触れたAPIドキュメントを生成するための元ネタになります。
メンテナンスする際や、GitHubなどでライブラリのソースコードを公開する際にも、使い方のヒントになりますので記述しておくと良いです。

```javascript:JSDocドキュメンテーションの例
/** 変数の説明 */
var variable1 = false
/** 変数の説明 */
let variable2 = false
/** 定数の説明 */
const CONSTANT1 = false

/**
 * 関数の説明
 * @param {int} arg1 - 引数の説明
 * @param {int} arg2 - 引数の説明
 * @return {bool} - 戻り値の説明
 */
function f(arg1, arg2) {
  ...
  return true
}

/**
 * クラスの説明
 */
class Foo {
  /**
    * コンストラクタ
    * @param {int} argInt - 引数の説明
    * @param {string} argString - 引数の説明
    * @param {bool} argBool - 引数の説明
    * @param {Array|Object} argAny - 引数の説明
    */
  constructor(argInt, argString, argBool, argAny) {
     ...
  }

  /**
   * メソッドの説明
   * @param {int} a - 引数1の説明
   * @param {int} b - 引数2の説明
   * @return {int} - 戻り値の説明
   */
  foo(a, b) {
    return a + b
  }

  /**
   * hogeのgetter
   * @return {string} - hogeプロパティ値
   */
  get hoge() {
    return this._hoge
  }

  /**
   * hogeのsetter
   * @param {string} value - hogeプロパティにセットする値
   */
  set hoge(value) {
    this._hoge = value
  }
}

```

---

### 4-3-2. コード補完

Google Apps Scirptのスクリプトエディタにはコード補完の機能があります。
ライブラリについてもコード補完することが可能ですが、残念ながら完全ではありません。
ライブラリ内にclassを定義して利用してもらう場合には注意が必要です。

---

注意点については検証を交えながら解説していきますが、先に結論を記載します。

---

```
・ ライブラリ内のglobalなfunctionはコード補完が効く
・ classについては補完に問題がある

従って、以下のいずれかの方針でライブラリの作成を検討する。

・ コード補完の完全性までを考慮してライブラリを作成するのであれば、classは使わずにglobalなfunctionのみで構成する
・ ライブラリのAPIを別途ドキュメントで補うという方針であれば、classを使っても問題はない
```

---

参考）TypeScriptベースですが、こちらの記事でも検証されていますので、リンクを掲載させていただきます
[Google Apps Script でクラス型のコードを書いたさいのスクリプトエディタでの補完への対処方法 (Bad Hack) - ChangeLog - noissefnoc](http://noissefnoc.hateblo.jp/entry/2019/04/21/190000?utm_source=feed)


---

前述の2)のコードの補完は以下のようになります。

2) クラスを取得するsuper globalな関数を作成する

```javascript:ライブラリ側（識別子=Lib）
class Foo {
  constructor() {
  }
}

function foo() { return Foo } // クラスを取得する
```

---

検証結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/0d3eb9cc-1310-4949-37d9-1ff023adbd32.png)

foo()は認識されましたが、戻り値の型がvoidになっています。

---

JSDocを追加してみます。

```javascript:ライブラリ側（識別子=Lib）
class Foo {
  constructor() {
    this._va = 0
  }
}

/**
 * Fooクラスを取得する
 * @return {Foo} - Fooクラス
 */
function foo() { return Foo } // クラスを取得する
```

---

検証結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/99292544-aaa8-e34e-524d-49d27b436ee8.png)

戻り値の型がドキュメント通りになりました。

---

次に、Fooクラスのメソッド、プロパティが補完されるかを検証します。

```javascript:ライブラリ側（識別子=Lib）
class Foo {
  constructor() {
    this._bar = 0
  }

  /**
   * hogeメソッド
   * @return {bool} - true固定
   */
  hoge() {
    return true
  }
  
  /**
   * barプロパティのgetter
   * @return {int} - vaプロパティの値
   */
  get bar() {
    return this._bar
  }

  /**
   * barプロパティのsetter
   * @param {int} value - vaプロパティにセットする値
   */
  set bar(value) {
    this._bar = value
  }

}

/**
 * Fooクラスを取得する
 * @return {Foo} - Fooクラス
 */
function foo() { return Foo } // クラスを取得する
```

---

検証結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/bd020dce-fc33-f26a-e786-cf2f46abd12d.png)
残念ながら、何も補完されませんでした。

---

さて、ではどのようにすれば補完されるでしょうか。
以下の方法で補完されるようになります。

1. Fooクラスのインスタンスを生成するfunctionを作成、さらにJSDocの戻り値の型は識別子にする
2. Fooクラスのメソッドをglobalなfunctionとして宣言、さらにJSDocもメソッドと同じ内容にする

---

```javascript:ライブラリ側（識別子=Lib）

/**
 * Fooインスタンスを生成して取得する
 * @return {Lib} - Fooインスタンス
 */
function create() {
  return new Foo() // Fooクラスのインスタンスを生成して返却
}

/**
 * hogeメソッド
 * @return {bool} - true固定
 */
function hoge() {
  throw Exception() // Lib.hoge()と呼ばれてしまうのでExceptionをthrowする
}
```

---

検証結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/93ee4f20-ffce-9660-bf6b-68504e206882.png)
hogeが補完されるようになりました。

---

しかしながら、この方法には副作用があります。
先に完全ではないと述べた理由がこちらです。

Fooインスタンスからの補完にcreateメソッドが表示されてしまう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/93ee4f20-ffce-9660-bf6b-68504e206882.png)
Lib識別子からの補完にhogeメソッドが表示されてしまう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/37a62b18-3bbf-eb12-3e7a-15e734d7ace1.png)

---

余計なものが表示されてしまう上に、これらは全て実行可能であるため、予期せぬトラブルの原因にもなります。
さらに言うと、重複したようなコードとコメントを書かなければならないため、ライブラリの改修コストも上がります。

---

ちなみに、ES5仕様でオブジェクト指向実装をした場合ですが、classと同じようなJSDocを書いても補完されません。

```javascript:ES5での定義
/**
 * @return {Lib} - FooES5
 */
var FooES5 = function() {
}

/**
 * hogeメソッド
 * @return {bool} - true固定
 */
FooES5.prototype.hogeES5 = function() {
  return true
}
```

---

検証結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/abb0ff60-89f4-55d0-f335-775672a5a217.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/f02e30e3-342d-00c2-fdb4-0f40ae9653ad.png)

---

これまでの検証より、以下のことがわかります。

- **ライブラリ内のglobalなfunctionはコード補完が効く**
- **classについては補完に問題がある**

従って、コード補完の完全性までを考慮してライブラリを作成するのであれば、classは使わずにglobalなfunctionのみで構成する、といった作り方になるでしょう。
ライブラリのAPIを別途ドキュメントで補うという方針であれば、classを使っても問題はないでしょう。

ライブラリを作成する際には上記の点について注意が必要です。

---

# 5. おわりに

作成したライブラリはclaspでダウンロードしてGit上で管理しておくと良いです。
以下の記事が参考になりますのでリンクを掲載させていただきます。Bitbucketで管理する方法が書かれています。
[claspを使い、Google Apps Scriptプロジェクトをgitでバージョン管理する](https://qiita.com/rf_p/items/7492375ddd684ba734f8)

何度も同じコードを実装するのはトータルコストが上がりやすい傾向にありますので、ライブラリの活用をオススメします。

読んでいただいて、ありがとうございました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/94fd031b-0c09-4f37-116f-c05271fdd7f9.png" width="200">
弊社は業務効率化・自動化など、仕組みで解決するお手伝いをさせていただいております。
お仕事のご依頼はコチラ↓までお願いいたします。

[株式会社シクミヤ](https://shikumiya.co.jp/)
note: [Visionary Base編集部](https://note.com/visionarybase)
Twitter: @shikumiya_hata