GASでWebアプリ作るときのTips

## 目的

初めまして。初投稿です。駆け出しの IT エンジニアです。
最近業務で GAS を触っていたので、得た知見をまとめてみました。
やってるといろいろと悩むことも多いので、僕が調べた内容を記載しておきます。
内容に間違いなどありましたら、ご指摘いただけると幸いです。

書いていると長くなっちゃいました。通しで読むと、同じようなコードが何回も出てきます。
できるだけ、必要な箇所だけ読んで、完結するようにしたためです。
これから始める人の参考になれば最高です！！

:::note info
この記事の応用編でもある、フォーム作成の記事を書きました！
[GASでいい感じのHTMLフォームを作成する - Qiita](https://qiita.com/dadadaiiiiiii/items/3786af69f7f65d45dbdc)
:::

---

## GAS とは

GAS(Google Apps Script)は無料で利用できる Google のローコードプラットフォームです。
Microsoft でいう VBA 的なことが記述できるところです。
SpreadSheet や Form, Document など Google のサービスに独自のアドオンを付けたいときは便利なサービスです。

GAS では簡単な Web アプリケーションを作成することも可能です。
この記事では GAS 初心者の僕が Web アプリケーション作成する際に悩んだ点をまとめております。

---

## 基本的な操作

プロジェクトの作成は、Google Drive から新規で Google Apps Script を選択すると作成できます。
（clasp を利用したローカル開発環境の構築は下の方に書いています）

Web アプリケーションの基本的な作り方としては、doGet か doPost 関数を定義してあげればよいです。
それらの関数から、HtmlOutput オブジェクトを返してあげればよいです。
下記の gs コードと html ファイルを作成して、新しいデプロイを選ぶと起動できます。

```javascript:main.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index');
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h1>Hello GAS!</h1>
  </body>
</html>
```

簡単でしょ？

ちなみに gs ファイルは javascript の書き方と同じなので、下記のような書き方もできます。

```javascript:main.gs
const doGet = () => {
    return HtmlService.createHtmlOutputFromFile('index');
}
```

お好きな書き方でどうぞ。

---

## サーバーサイド

上記内容で、基本的な操作はできるようになった(?)と思いますが、GAS ならではの書き味の部分も多いです。
僕が悩んだ箇所を中心にそれらをまとめてみました。

### title や meta タグをつける

html 内の head タグの中に title 書いても変わりません。
出力する前に gas の中で指定してあげる必要があります。
これは meta タグも同様です。
ブラウザで確認するとわかるのですが、出力された html は iframe で表現されています。
なので、title や meta タグなどを html に直接記述しても思った通りには反映されないので注意です。

```javascript:main.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
            .setTitle('タイトルはここだよ')
            .addMetaTag('viewport', 'width=device-width, initial-scale=1');;
}
```

---

### 自分の URL を取得する

このコードよく使うけど、新しいプロジェクトを作るたびに忘れます。

```javascript:main.gs
function getAppUrl() {
    return ScriptApp.getService().getUrl();
}
```

ちなみに開発終わってデプロイして URL が変わらなくなったら、固定値を返した方が早いと思います。

---

### セッションユーザーのメールアドレス情報を取得する

```javascript:main.gs
function getUserEmail() {
    const user = Session.getActiveUser(); // スクリプトの実行者を取得
    return user.getEmail();
}
```

これでメールアドレスの情報が取得できます。
ContactApp を利用すれば、名前などを取得することもできます。
ですが、スクリプト実行者（デプロイ時に指定可能）の連絡先に、氏名がない人の場合（自分自身などもない場合はある）など、うまく取れないことも多いので僕はあんまり使っていません。
詳しくは[参考情報](https://jill-tone.com/gas-getfullname/)から見てみてください。

---

### ページを分ける

GAS は基本的にパスを切ることができません。
なのでページ遷移させるときは、工夫が必要です。
ページ毎に GAS プロジェクトを立ち上げて、リンクを貼るのもよいです。

その方法だと少し面倒なのと、管理が大変です。
なので僕はクエリパラメータを使って、ページ遷移を実現することがほとんどです。
以下サンプルです。

```javascript:main.gs
function doGet(e) {
  const page = (e.parameter.page || "index");
  return HtmlService.createTemplateFromFile(page).evaluate();
}

function getAppUrl() {
    return ScriptApp.getService().getUrl();
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h1>Hello GAS!</h1>
    <a href="<?= getAppUrl() ?>?page=page1">page1へ</a>
  </body>
</html>
```

```html:page1.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h1>page1だよ！</h1>
    <a href="<?= getAppUrl() ?>">TOPへ</a>
  </body>
</html>
```

これで`index.html`と`page1.html`のページ遷移を行うことができました。
`e.parameter.page`で、クエリパラメータの中の`page`を見つけてきて、変数にセットしてあげています。
なかったら index が入ります。html ファイルの中で

```html
<?= ?>
```

で囲まれている部分は、GAS で利用できるテンプレートエンジンです。
gs ファイルで定義した関数を呼び出すことができます。

ここでは`getAppUrl関数`を呼び出して自分自身の URL を取得してリンク先を指定しています。
そしてテンプレートを利用する際は
`HtmlService`の`createTemplateFromFile`メソッドに`htmlファイル名`を渡してあげ、
`evaluate`することで`HtmlOutputオブジェクト`が生成されます。
こちらも先ほどまでの、`createHtmlOutputFromFile`メソッドと同様に`setTitle`や`addMetaTag`が利用できます。

---

### html テンプレートに変数を渡す

html テンプレートは直接 gs ファイルの関数を参照するだけでなく変数を渡すこともできます。以下サンプルです。

```javascript:main.gs
function doGet(e) {
  const html = HtmlService.createTemplateFromFile('index');
  const human = {
    firstname: "Taro",
    lastname: "Okamoto"
  }
  html["human"] = human;
  return html.evaluate();
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h1>Hello</h1>
    <h1><?= human.firstname ?></h1>
    <h1><?= human.lastname ?></h1>
  </body>
</html>
```

また、以下の書き方でも同様の出力が可能です。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h1>Hello</h1>
    <?
    output.append('<h1>' + human.firstname + '</h1>');
    output.append('<h1>' + human.lastname + '</h1>');
    ?>
  </body>
</html>
```

output.append で HTML 内に評価された値が出力されます。
毎行書きたくない場合は、下記でも同様の結果になります。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h1>Hello</h1>
    <?
    output.append(
      '<h1>' + human.firstname + '</h1>'
    + '<h1>' + human.lastname + '</h1>'
      );
    ?>
  </body>
</html>
```

---

#### for や if も使えます

基本的に

```html
<? ?>
```

で囲まれた部分は、普通に javascript 書いたら大体動きます。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h1>Hello</h1>
    <?
    for(let i=0; i<10; i++){
      if(i != 5){
        output.append('<h1>' + i + '</h1>');
      }
    }
    const array = ["a","b","c","d"];
    array.forEach((item) => {
      output.append('<h1>' + item + '</h1>')
    });
    ?>
  </body>
</html>
```

---

### 他ファイルを参照する

記述量が増えてくると、css とか js などは、別ファイルに記述したいことがよくあると思います。
その場合 GAS だと、`link`や`script src`などは、思った通りに動いてくれません。

GAS は一度 HTML にレンダリングされたら、
別ファイルを参照しに行くことは（基本）できません。
なので、同じページ内に、同時にレンダリングしてあげる必要があります。
以下、サンプルです。

```javascript:main.gs
function doGet(e) {
  const html = HtmlService.createTemplateFromFile('index');
  const human = {
    firstname: "hoge",
    lastname: "fuga"
  }
  html["human"] = human;
  return html.evaluate();
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
  </head>
  <body>
    <h1>Hello</h1>
    <button id="button">ボタン</button>
    <?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?>
  </body>
</html>
```

```html:css.html
<style>
  h1 {
    color: red;
  }
</style>
```

```html:js.html
<script>
  document.getElementById('button').addEventListener('click', function() {
    alert("ボタンが押されたよ");
  })
</script>
```

ここで js や css は html ファイルにする必要があります。
そもそも GAS では`.gs`ファイルと`.html`ファイルしか保管できません。
なので html ファイルの中で sytle や script タグで囲ってやることで、ファイルの読み込みを実現しています。

あとここで

```html
<?!= ?>
```

という記述をしています。これは`Force-printing scriptlets`と呼ばれます。
`<`とかの特殊文字をそのまま出すか、文字列に変換するかの違いです。
例えば、下記のようなサンプルを動かすと理解できると思います。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <?= '<h1>aaa</h1>' ?>
    <?!= '<h1>bbb</h1>' ?>
  </body>
</html>
```

これを表示すると上は`<h1>aaa</h1>`という文字列が出力されます。
一方、下段は html タグとして、認識されて出力されます。
なので html を include したいときは、`<?!= ?>`という表現で実現できます。

---

### 他ファイルを参照する（パラメーターを含む）

上記の書き方だと、パラメータを含む html ファイルを作成することができません。
なので「html に変数を渡す」セクションでも書いたように、createTemplateFromFile で html を読み込んで、
変数を渡し、評価してあげないといけないです。

[ここ](https://qiita.com/kyamadahoge/items/5c916d57f2dce3693c26)で紹介されているように、include 用の関数を作ってあげるのがよいです。
この include 関数を利用すると、html ファイルの記述はすっきりします。
以下サンプルです。

```javascript:main.gs
function doGet() {
  const html = HtmlService.createTemplateFromFile('index');
  return html.evaluate();
}

// https://qiita.com/kyamadahoge/items/5c916d57f2dce3693c26
// 上記サイトから引用
function include(filename, params) {
  const template = HtmlService.createTemplateFromFile(filename);
  if (params) {
    for (const key in params) {
      template[key] = params[key];
    }
  }
  return template.evaluate().getContent();
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= include('css'); ?>
  </head>
  <body>
    <?!= include('hello', { name : "Taro" }); ?>
    <button id="button">ボタン</button>
    <?!= include('js'); ?>
  </body>
</html>
```

```html:hello.html
<h1><?= name ?></h1>
```

```html:css.html
<style>
  h1 {
    color: red;
  }
</style>
```

```html:js.html
<script>
  document.getElementById('button').addEventListener('click', function() {
    alert("ボタンが押されたよ");
  })
</script>
```

---

### スクリプトプロパティを使う

コード内に書きたくない情報とかを書くのに便利です。
SpreadSheet や DB から引っ張ってくると、遅いし時間もかかります。
大事な情報は、ここに書くとよいかと思います。

コードからでも取得、設定できますし、手動でも設定可能です。
手動で設定する場合は GAS を開いて左側の歯車ボタンの下の方に行くとあります。
コードから設定する場合は、下記サンプルのやり方で可能です。

```javascript:main.gs
function setPropertyVal() {
  PropertiesService.getScriptProperties().setProperty("ARTIST", "岡本太郎");
}

function getPropertyVal() {
  const artist = PropertiesService.getScriptProperties().getProperty("ARTIST");
  Logger.log(artist);
}
```

スクリプトプロパティには期限はないので、ずっと保存しておきたい値はこちらに保存するとよいです。

---

### キャッシュを使う

GAS を使っていろいろ処理を書いていくと、処理が重くなる時があります
（スプレッドシート触る処理とかは重くなりがちだと思います）。
仕方ない部分は仕方ないですが、キャッシュできる部分はキャッシュしておくと処理がかなり早くなります。

また[ここ](https://qiita.com/golyat/items/ba5d9ce38ec3308d3757)に書いているよう、に put 時に`JSON.stringify`、get 時に`JSON.parse`してやると使い勝手が良いです。
キャッシュは最長 6 時間まで設定できます。

あとこれは GAS 側（サーバー側）のキャッシュなので、ブラウザ側のキャッシュとは違います。
[ここ](https://qiita.com/golyat/items/ba5d9ce38ec3308d3757)に詳しく乗っています。以下サンプルです。

```javascript:main.gs
function doGet(e) {
  const html = HtmlService.createTemplateFromFile('index');
  const scriptCache = makeScriptCache();
  let result = scriptCache.get('result');
  if(!result){
    // キャッシュが存在しない場合は処理を実行
    result = heavyProcess();
    scriptCache.put('result', result);
  }
  html['result'] = result;
  return html.evaluate();
}

// https://qiita.com/golyat/items/ba5d9ce38ec3308d3757
// 上記記事から引用
function makeScriptCache() {
  const scriptCache = CacheService.getScriptCache();
  return {
    get: function (key) {
      return JSON.parse(scriptCache.get(key));
    },
    put: function (key, value, sec) {
      //リファレンスよりcache.putの3つ目の引数は省略可。
      //デフォルトでは10分間（600秒）保存される。最大値は6時間（21600秒）
      scriptCache.put(
        key,
        JSON.stringify(value),
        sec === undefined ? 600 : sec
      );
      return value;
    },
  };
}

// 処理に時間がかかる関数
function heavyProcess() {
  Utilities.sleep(10000);
  return 'Taro';
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h1><?= result ?></h1>
  </body>
</html>
```

上記のサンプルでは、重たい処理の結果を画面に出力しています。
1回目は10秒かかってしまいますが、2回目以降はすぐに結果が返ってきます。

ただし、結果が動的に変わるような場合は、工夫が必要です。

動的に値が変わるような処理結果であっても、
前回のキャッシュが残っていれば、値は変わらずに表示されてしまいます。
なので
・変わらない部分だけキャッシュしておく
・変わった時は自動でキャッシュの値も変更される
などの処理を、適宜書いておく必要があります。

また、上記の例では`getScriptCache()`を使っています。
この場合、どのユーザーでアクセスしてもキャッシュの内容は変わらないです。
一方、`getUserCache()`を使うと、ユーザーごとに個別のキャッシュを設定することができます。
ログイン処理などを行う時は、利用しやすいと思います。

---

:::note warn
下記の「クライアントサイドからやっちゃう」セクションで記載していた方法を、再度検証したのですが、どうもうまく動かない（動かなくなった？）ようで、記述していたコードは削除しました。

終わらないような場合は、時間を取得して、制限時間超えそうな場合はTriggerなどを作成して、処理を渡すのが今のところの最善のような気がしています。

よいやり方があれば教えてください。
:::

### ~~クライアントサイドからやっちゃう~~

~~重い処理を書いたとき、GAS の実行時間の制限（６分）に引っかかってしまうことがあります。~~
~~上記の工夫をしても何ともならないこともあると思います。~~
~~そういう場合はブラウザ側から GAS の関数を実行すると、制限に引っかからずに実行することができます。~~

~~やり方は、下の方に記述している、~~
~~`google.script.run`を、クライアントサイドの javascript から起動してあげると、処理を走らすことができます。~~
~~([こちら]に、並列化なども含めて詳しく書かれています)~~

~~以下、サンプルです。~~


~~このやり方であれば、制限時間に引っかかることなく処理を実行できます。~~
~~ただブラウザから開いてあげる必要があるので、バッチ処理とかには向いていないです。~~
~~バッチで処理したい場合は、`Selenium`などで、ブラウジングしてあげるとよいかと思います。~~

---

### ログ出力

ログ出力には下記のように console クラスと Logger クラスが用意されています。
用途に合わせてお使いください。

```javascript:main.gs
function LogTest(){
  console.log("console.log");
  console.info("console.info");
  console.warn("console.warn");
  console.error("console.error");
  Logger.log("Logger.log");
  Logger.log("Logger.log format %s", 1);
  Logger.log(Logger.getLog());
}
```

---

## クライアントサイド

### 画像を挿入する

画像を挿入するのも少しクセがあります。
そもそも GAS 内に画像を保存することができません。
なので GoogleDrive 上に画像を保存し、そのアクセス権限を緩くして、参照する方法をとります。

[ここ](https://tonari-it.com/gas-web-html-img-drive/)を見ながら画像をドライブに保存し、ID を取得しましょう。
あとは下記のように html 上で指定してあげれば表示可能です。めんどくさいですね。

```html
<img src="https://drive.google.com/uc?id=取得したID" />
```

---

### サーバーサイド（gs ファイル）で定義した関数を呼び出す

javascript 内に`google.script.run`を記述すればよいです。下記はサンプルです。

```javascript:main.gs
function doGet() {
  const html = HtmlService.createTemplateFromFile('index');
  return html.evaluate();
}

function testLog(str) {
  Logger.log(str);
}

function include(filename, params) {
  const template = HtmlService.createTemplateFromFile(filename);
  if (params) {
    for (const key in params) {
      template[key] = params[key];
    }
  }
  return template.evaluate().getContent();
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= include('css'); ?>
  </head>
  <body>
    <?!= include('hello', { name : "Taro" }); ?>
    <button id="button">ボタン</button>
    <?!= include('js'); ?>
  </body>
</html>
```

```html:js.html
<script>
  document.getElementById('button').addEventListener('click', function() {
    google.script.run.testLog("テストです");
  })
</script>
```

これでボタンを押すと、GAS 側の実行ログに`テストです`が表示されます。

---

### サーバー側で処理した結果を受け取る

上記の例だとサーバー側で処理して終わりです。
ブラウザ側で、処理した結果を使いたいときもあると思います。
その時は`withSuccessHandler`や`withFailureHandler`を使用することで、結果を利用することができます。
下記はサンプルです。

```javascript:main.gs
function doGet() {
  const html = HtmlService.createTemplateFromFile('index');
  return html.evaluate();
}

function testFunc() {
  const num = Math.floor(Math.random()*10)
  if ( num > 5) {
    return num;
  } else {
    throw new Error;
  }
}

function include(filename, params) {
  const template = HtmlService.createTemplateFromFile(filename);
  if (params) {
    for (const key in params) {
      template[key] = params[key];
    }
  }
  return template.evaluate().getContent();
}

```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= include('css'); ?>
  </head>
  <body>
    <?!= include('hello', { name : "Taro" }); ?>
    <button id="button">ボタン</button>
    <?!= include('js'); ?>
  </body>
</html>
```

```html:js.html
<script>
  document.getElementById('button').addEventListener('click', function() {
    google.script.run.withSuccessHandler(function(data) {
      alert("成功です。値は" + data + "です。" );
    }).withFailureHandler(function() {
      alert("失敗です");
    }).testFunc();
  })
</script>
```

上記のサンプルでは
・サーバーサイドで適当に作った乱数が５より大きかったら値を返す
・５以下だった場合はエラーを返す関数を呼び出しています。
その返却値を見て、クライアントサイドでアラートを出す処理を行っています。

---

### async/await 使えるようにする

上記の`google.script.run`で記述してもいいのですが、コールバック地獄になりがちです。
なので、プロミスで包んであげます（[ここ](https://zenn.dev/37cohina/articles/dc88cefac79ab1)に書いてます）。
下記はサンプルです。

```javascript:main.gs
function doGet() {
  const html = HtmlService.createTemplateFromFile('index');
  return html.evaluate();
}

function myFunction1() {
  const num = Math.floor(Math.random()*10);
  Utilities.sleep(2000);
  return num;
}

function myFunction2(num) {
  Utilities.sleep(2000);
  return num*2;
}

function include(filename, params) {
  const template = HtmlService.createTemplateFromFile(filename);
  if (params) {
    for (const key in params) {
      template[key] = params[key];
    }
  }
  return template.evaluate().getContent();
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= include('css'); ?>
  </head>
  <body>
    <?!= include('hello', { name : "Taro" }); ?>
    <button id="button">ボタン</button>
    <?!= include('js'); ?>
  </body>
</html>
```

```html:js.html
<script>
  // google.script.runのプロミス化
  // https://zenn.dev/37cohina/articles/dc88cefac79ab1
  // 上記の記事から引用
  const ServerScript = {};
  for ( const methodName in google.script.run ) {
      const method = google.script.run[ methodName ];
      if ( typeof method !== "function" ) continue;
      if ( methodName === method.prototype.constructor.name ) continue;
      ServerScript[ methodName ] = ( ...args ) => new Promise(
          ( resolve, reject ) => {
              google.script.run
              .withSuccessHandler( resolve )
              .withFailureHandler( reject )
              [ methodName ]( ...args )
          }
      );
  }

  document.getElementById('button').addEventListener('click', function() {
    (async ()=> {
      const data1 = await ServerScript.myFunction1();
      console.log(data1);
      const data2 = await ServerScript.myFunction2( data1 );
      console.log(data2)
    })();
  });
</script>
```

async/await ではなく、プロミスチェーンでも書けます

```html:js.html
<script>
  // google.script.runのプロミス化
  const ServerScript = {};
  for ( const methodName in google.script.run ) {
      const method = google.script.run[ methodName ];
      if ( typeof method !== "function" ) continue;
      if ( methodName === method.prototype.constructor.name ) continue;
      ServerScript[ methodName ] = ( ...args ) => new Promise(
          ( resolve, reject ) => {
              google.script.run
              .withSuccessHandler( resolve )
              .withFailureHandler( reject )
              [ methodName ]( ...args )
          }
      );
  }

  document.getElementById('button').addEventListener('click', function() {
    ServerScript.myFunction1()
      .then((data1) => {
        console.log(data1);
        return ServerScript.myFunction2( data1 );
      }).then((data2) => {
        console.log(data2);
      });
  });
</script>
```

これですっきりしますね。

---

### Web Worker を使う

一応ですが、Web Worker も利用できます。
これで並列処理も可能です。
以下サンプルです。

```javascript:main.gs
function doGet() {
  const html = HtmlService.createTemplateFromFile('index');
  return html.evaluate();
}


function include(filename, params) {
  const template = HtmlService.createTemplateFromFile(filename);
  if (params) {
    for (const key in params) {
      template[key] = params[key];
    }
  }
  return template.evaluate().getContent();
}

function getWorkerCode(jsfile){
  return HtmlService.createTemplateFromFile(jsfile).evaluate().getContent();
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= include('css'); ?>
  </head>
  <body>
    <div id="webWorkerContent" class="container-fluid d-none">
      <h1>Web worker Test page</h1>
      <div class="content-wrapper">
        <div class="worker-thread d-flex justify-content-start">
          Worker Threadでfetchしてきた情報をconsoleと画面に表示する。
          <button id="workerThreadBtn" class="btn btn-primary">実行</button>
          <div id="workerThreadResult"></div>
        </div>
      </div>
    </div>
    <script>
      var worker;
      var mainThreadResult = document.getElementById('mainThreadResult');

      google.script.run.withSuccessHandler( code => {
        const url = URL.createObjectURL( new Blob([code], {type:"text/javascript"}));
        worker = new Worker(url);
      }).withFailureHandler(console.error)
      .getWorkerCode("worker.js");

      // worker threadの実行
      document.getElementById('workerThreadBtn').addEventListener('click', function() {
        worker.onmessage = function(event) {
          document.getElementById('workerThreadResult').textContent = event.data;
          console.log('Got: ' + event.data + '\n');
        };
        worker.onerror = function(error) {
          console.log('Worker error: ' + error.message + '\n');
          throw error;
        };
        worker.postMessage('Workerに送ったデータだよ！');
      });
    </script>
  </body>
</html>
```

```html:worker.js.html
self.addEventListener('message',(e) => {
  self.postMessage(e.data);
}, false);
```

worker のファイルなどは直接は呼び出せないので`google.script.run`で`getWorkerCode`から取得しています。
取得したコードを Blob(Binary Large Object)にして URL を作成し、それを new Worker に渡しています。Inline Worlker と呼ばれます。
[ここ](https://web.dev/workers-basics/#toc-inlineworkers)に詳しく書かれています。

service worker は gas ではたぶん使えません。起動自体はできるのですが、ほかのファイル見に行けないのであんまり意味ないです。

---

## 環境構築

### URL を変更せずに新バージョンをデプロイする

[ここ](https://ryjkmr.com/gas-web-app-deploy-new-same-url/)に詳しく書いています。

やり方としては、デプロイを管理から編集（鉛筆マーク）押して、バージョンを新バージョンにする。
あとは実行ユーザーとアクセスできるユーザーを設定して OK。

### 「appsscript.json」ファイルを表示する

デフォルトでは表示されていないので、GAS の左のメニューの歯車マークから設定を開きます。
全般設定の中にある「表示する」旨の選択肢があるのでチェックを付けて、エディタに戻ると表示されています。

### clasp を利用する

ブラウザ上で作業するには限界があるので、ローカル開発環境を整えます。
[ここ](https://dev.classmethod.jp/articles/vscode-clasp-setting/)とかに情報あります。

ざっくり書くと
1\. node をインストールする
2\. [ここ](https://script.google.com/home/usersettings)から Google Apps Script API をオンにする。
3\. npm を利用して必要なパッケージをインストールする。

```powershell
npm init -y
npm install @google/clasp -g
npm install @types/google-apps-script
```

4\. Google アカウントでログインする

```powershell
clasp login --no-localhost
```

指示が出るので、よしなにする。

5\. プロジェクト作成

```powershell
clasp create
```

6\. プッシュ

```powershell
clasp push
```

7\. GAS を開く

```powershell
clasp open
```

8\. push するディレクトリの指定
プロジェクト作成すると clasp.json が作成されると思うので開いて

```json
{"scriptId":"スクリプトのID","rootDir":"src"}
```

とかにすると、src ディレクトリ以下のファイルが push される

9\. typescript とかで開発したい人は[ここ](https://panda-program.com/posts/clasp-typescript)とか見ながらやる。

10\. css とか js を push 時に html ファイルに変換してくれるシェルスクリプトがあると便利
[ここ](https://qiita.com/m-yamashita/items/2c484b3c0504c03dea8a)に詳しいことが書いてあります。
これでローカル開発時は `css` とか `js` ファイルとして開発できるようになるので、シンタックスハイライトや lint がいい感じになります。
シェルスクリプトなので powershell の人はうまく動かないかもしれないです。
その時は powershell ように書き換えるとよいです。

11\. Typescript でクライアントサイドの型定義ファイルを使う
プロジェクト内に`@types`ディレクトリを作成して、これ（[google.script.d.ts](https://gist.github.com/DominikPalo/bea3d958877566fe24ebe5ee1688d976)）を置いておくと、
クライアントサイドの google スクリプト（withSuccessHandler など）の型定義を使用できるようになります。
[ここ](https://gist.github.com/kemsakurai/58bc3ff86df43adde5fc4a2f27a6ca98)に詳しく書いてくれています。

---

### ファイルの順番を変える

ブラウザのエディタからだと、`a-z`にソートすることは可能です。
ただ、上から順に読み込まれていくので、ファイルが増えると整理が大変ですが、頑張るしかないです。

clasp を利用する場合は、ファイルの読み込み順を指定することができます。
`clasp.json`を開いて、

```json
  "filePushOrder": ["taro.js", "okamoto.js"]
```

と記載してあげればよいです。
配列の順番にファイルを読み込んでくれます。
ここに記載されていないファイルは、記載されているファイルの後に、
アルファベット順に読み込まれます。

---

### 連携したスプレッドシートなどを開く

通常```clasp open```した際には、Apps Scriptのページが開きます。
しかし、スプレッドシートやフォームなどを直接見に行きたい場合もあると思います。
その場合は、```.clasp.json```ファイルを下記のように編集します。

```json
{
  "scriptId":"スクリプトのID",
  "rootDir":"src",
  "parentId": [
    "スプレッドシートのID"
  ]
}
```

そのうえで、```--addon```オプションを指定してあげると、指定したIDのファイルを開くことができます。

```powershell
clasp open --addon
```

配列になっていますが、自動で開かれるのは最初の1個目だけです。
コンソール上では複数指定すると、対応する複数のURLが表示されるので、
適宜そこからリンクを踏めば、指定のファイルを開くことができます。

フォルダを指定することもできるので、
複数ファイルがある場合は、親フォルダのIDを指定すると便利です。

---

## 最後に

しばらく GAS を触っていたので、知識をまとめることができ、また次触るときのためになったかなと思います。
書いていると少し長くなってしまいました。
ここまで読んでくれた人は感謝です。

見直していると、記事が消えている箇所がちらほら見受けられるます。
頑張って修復しましたが変なところもあるかもしれません。
ブラウザで書かない方がよいですね。
この記事が誰かの役に立てば最高です。

---

## 参考資料

・[公式サイト(Google Apps Script)](https://developers.google.com/apps-script)
・[公式サイトの HTML サービスの説明](https://developers.google.com/apps-script/guides/html)
・[公式のテンプレートガイド](https://developers.google.com/apps-script/guides/html/templates)

画像を挿入する
・[GAS の Web ページに Google ドライブ内の画像を挿入する方法](https://tonari-it.com/gas-web-html-img-drive/)

ページ遷移する
・[【GAS】Web アプリでページ遷移させる方法](https://somachob.com/gas-page-transition/#toc2)

ContactApp を利用して名前を取得する
・[スクリプト実行者の氏名を取得する方法 – Google Apps Script](https://jill-tone.com/gas-getfullname/)

外部ファイルの読み込み
・[Google Apps Script (GAS)の Scriptlets を使って外にある JavaScript や CSS を読み込む](https://codeaid.jp/gas-html/)

GAS で別の HTML ファイルをインクルードする
・[GAS で別の HTML ファイルをインクルードする](https://qiita.com/kyamadahoge/items/5c916d57f2dce3693c26)

キャッシュの話
・[【GAS】GAS で値を保存して次の実行時でも利用する](https://qiita.com/golyat/items/ba5d9ce38ec3308d3757)

スクリプトプロパティの話
・[GAS のスクリプトプロパティをコードで設定する方法(setProperty メソッド)](https://auto-worker.com/blog/?p=2365#)

typescript で開発時の css や js ファイルの変換シェルスクリプト（そのほかも勉強になるので読んでおいて損なし）
・[GAS の落とし穴と、clasp で "WebApp" をモダンで快適に開発するために色々やった知見まとめ](https://qiita.com/m-yamashita/items/2c484b3c0504c03dea8a)

Web Worker について詳しく書かれています。
・[Introducing WebSockets - Bringing Sockets to the Web](https://web.dev/workers-basics/#toc-inlineworkers)

Typescript でクライアントサイドの型定義を利用する
・[google.script.d.ts](https://gist.github.com/DominikPalo/bea3d958877566fe24ebe5ee1688d976)
・[Google Apps Script の client-side API の TypeScript 型定義ファイル](https://gist.github.com/kemsakurai/58bc3ff86df43adde5fc4a2f27a6ca98)

クライアント側での並列処理
・[【GAS】「起動時間の最大値を超えました」エラー時の対策 処理の並列化編](https://hajiritsu.com/gas-timeout-error-routine/)

claspのgit
・[GitHub/clasp](https://github.com/google/clasp)

claspのアドオンオプションの説明
・[【小ネタ】Claspで連携したGoogle スプレッドシートを開く](https://nainaistar.hatenablog.com/entry/2022/07/11/120000)