個人的GASでWebアプリを作る時の初手行動

# ファイル構造

gsファイル2つと、htmlファイル3つを用意します。

mainにはdoGet,doPostの2つの関数を、それ以外の補助関数はfunctionsに放り込んでいます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953722/b05443ac-e3f0-890f-12fc-615836fa3955.png)

## 各ファイルの中身

### main.gs

doGet,doPostの2つのみを記載するようにしています。eventHandlerについては後述します。

```js

function doGet(e){


}

function doPost(e){

    const content = JSON.parse(e.postData.content);

}

const eventHandler = {};
```

### functions.gs

補助で使用する関数を記載しています。
デフォルトでは以下の関数を入れるようにしています。

① gssConsole

GASのWebアプリではconsoleが表示されないことがあるので、console.logの代わりに使用しています。
gssConsole.setSheetでログを吐き出すシートを指定した後、gssConsole.log()を使います。
配列やオブジェクトもすべて文字列に変換してから出力します。

② returnJSON

JSON形式で返すときに、いちいち書くのがめんどくさいので入れています。

③ env

PropertiesServiceの記述が面倒なので入れています。
```js
//通常
PropertiesService.getScriptProperties().getProperty("hoge");
//env ver
env.hoge;
```
④Cache

envと同様の理由です。CacheServiceの記述が面倒なので入れました。
詳しくはこちら

https://qiita.com/toratosoine/items/9c17fea4083059649cd2


```js
const gssConsole = {

    setSheet(sheet){

        this.sheet = sheet;

    },

    log(...content){

        this.sheet.appendRow(content.map(v => JSON.stringify(v)));

    }

}

function retrunJSON(content){

    return ContentService.createTextOutput().setContent(JSON.stringify(content)).setMimeType(ContentService.MimeType.JSON);

}

const env = (() =>  new Proxy({e : PropertiesService.getScriptProperties()}, {
    get: (t, p) => t.e ? t.e.getProperty(p) : void 0,
    set: (t, k, v) => t.e ? t.e.setProperty(k, v) : void 0
}))();

const Cache = (function(){

    const ScriptCache = CacheService.getScriptCache();
  
    const parseJSON = str => {
  
      try{
  
        return JSON.parse(str);
  
      }catch{
  
        return str;
      }
  
    };
  
    return {
  
      /**
       * @return {array} キャッシュに登録されている全てのキーを取得します。[key1,key2,key3,...] 
       */
      getKeys(){
  
        return this.require("ALL_KEYS_OF_THIS");
  
      },
  
      /**
       * キーからデータを取得。
       * @param {string} 取得したいデータのキー。(省略可)
       * @param {array} 取得したいデータのキー一覧。(省略可)
       * @return {object} キーに対応するデータ。省略した場合は、キャッシュされている全てのデータ。
       */
      require(key){
  
        let all;
        if(typeof key === "string"){
  
          return parseJSON(ScriptCache.get(key));
  
        }else if(Array.isArray(key)){
  
          all = ScriptCache.getAll(key);
  
        }else if(!key){
  
          all = ScriptCache.getAll(this.getKeys());
  
        }
  
        const obj = {};
  
        for(const key in all){
          obj[key] = parseJSON(all[key]);
        }
        return obj;
      },
  
      /**
       * データを追加。
       *  
       * @param {string} key キー名
       * @param {object} key {key,value}の形にすること。
       * @param {} value  型はなんでも良い。(省略可)
       */
      exports(key,value){
  
        //""をキー名として許可すると、this.require()で区別がつかない。
        if(!key) throw new Error("第一引数は必須です。");
        if(key === "ALL_KEYS_OF_THIS") throw new Error("禁止ワードです。");
  
  
        //日付をJSONに変換する時の処理。デフォルトだと扱いにくいので上書き。
        Date.prototype.toJSON = function(){
  
            return Utilities.formatDate(this,"JST","yyyy-MM-dd hh:mm:ss");
  
        }
  
        if(typeof key === "string"){
  
          const keys = this.require("ALL_KEYS_OF_THIS") || [];
  
          ScriptCache.put(key,JSON.stringify(value),21600);
  
          keys.push(key);
  
          ScriptCache.put("ALL_KEYS_OF_THIS",JSON.stringify(keys),21600);
  
        }else if(typeof key === "object"){
  
          const keys = this.require("ALL_KEYS_OF_THIS") || [];
  
          for (let i in key){
            key[i] = JSON.stringify(key[i]);
            keys.push(i);
          }
          
          ScriptCache.put("ALL_KEYS_OF_THIS",JSON.stringify(keys),21600)
          ScriptCache.putAll(key,21600);
  
        }
  
        return this;
  
      },
  
      /**
       * データの削除。
       * @param {string} key 削除したいデータのキー名(省略可)
       * @param {array} key 削除したいデータの全てのキー名(省略可)
       * @param {} key 省略した場合、全てのデータを削除。
       */
      remove(key){
  
        if(typeof key==="string"){
  
          ScriptCache.remove(key);
          const keys = this.require("ALL_KEYS_OF_THIS") || [];
          this.exports("ALL_KEYS_OF_THIS",keys.filter(k => k!==key));
  
        }else if(Array.isArray(key)){
  
          ScriptCache.removeAll(key);
          const keys = this.require("ALL_KEYS_OF_THIS") || [];
          this.exports("ALL_KEYS_OF_THIS",keys.filter(k => !keys.includes(key)));
  
        }else if(!key){
  
          ScriptCache.removeAll(this.getKeys());
          this.exports("ALL_KEYS_OF_THIS",[]);
  
        }
  
        return this;
  
      }
  
    }
  })();
  ```

### index.html

GASの仕様上、script.js,style.cssと分けることができないので、すべてhtmlで書いたのちに、GASの機能で読み込んでいます。

```html
<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <?!= HtmlService.createHtmlOutputFromFile('style').getContent(); ?>
      </head>
      
      <body>
        <?!= HtmlService.createHtmlOutputFromFile('script').getContent(); ?>
      </body>
      
</html>
```

### script.gs

ここにjavascriptをいろいろ書きます。
google.script.runの処理を書くのが面倒なので、async/awaitで記載できるようにしています。

https://qiita.com/toratosoine/items/a1fa81b0c98da0e32490
```html
<script>

    //ajax
    const _gas = new Proxy({},{
        get(t,p){
            return (...a) => new Promise((r1,r2) => {
                google.script.run.withSuccessHandler(r1)
                                .withFailureHandler(r2)
                                [p](...a);
            })
        }
    });
</script>
```

### style.css
ここにcssを書きます。

```html
<style>
    
</style>
```

# 条件分岐について

doGet/doPostのパラメータに処理内容を入れる場合があると思いますが、
できる限りif文/switch文を使わないようにしています。
```js
function doGet(e){
    const { type } = e.parameter;

    eventHandler[type](e); //if(type === "reply"){}とは書かない

}

const eventHandler = {};
eventHandler.reply = function(e){
    //処理
};
```
typeの分岐が多くなったら、処理ごとにファイルを分割します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953722/164b21ac-13f8-246f-1175-e9cfef8bdabf.png)