GAS＋Vue.jsでサーバレスなWebアプリ③

#はじめに
GASの[HtmlService](https://developers.google.com/apps-script/reference/html/html-service)を使ってSPAライクなWEBアプリケーションを書くことになったのでその時のメモです。
[こちらの記事](https://qiita.com/kyhei_0727/items/f409ab53bac931d4eb77)の続きとなります。

前回までの記事
[GAS＋Vue.jsでサーバレスなWebアプリ①](https://qiita.com/kyhei_0727/items/f409ab53bac931d4eb77)
[GAS＋Vue.jsでサーバレスなWebアプリ②](https://qiita.com/kyhei_0727/items/9f94947438750fc060f6)

本記事では、サーバサイド側のGASから取得したスプレッドシートのデータをVueインスタンスを用いて描画してみたいと思います。

#今回のアウトライン
1. GAS関数呼び出しの基本
2. クライアントサイド実装
3. サーバサイド実装
4. 終わりに

#それではさっそく書いてみるよ。

###1. GAS関数呼び出しの基本
クライアントサイドからのGAS関数の呼び出しは大きく２つの方法があります。
一つは[前回の記事](https://qiita.com/kyhei_0727/items/9f94947438750fc060f6)でも少しだけ触れたのですが、テンプレートタグを用いる方法です。

`<?!= yourServerSideFunction() ?>`
こんな感じのタグ内ではサーバサイド側に定義した関数を呼び出せるというやつですね。

詳しい使い方は[公式のドキュメント](https://developers.google.com/apps-script/guides/html/templates)を参照いただけたらと思います。
ぶっちゃけスプレッドシートの内容を描画するだけなら、
こちらの方がシンプルかもしれません。

もう一つがクライアントサイドのJavaScriptで`google.script.run`を用いる方法です。
今回はGAS+Vue.jsということで、こちらを少し見ていきたいと思います。
基本的にはクライアントサイドに定義したJavaScriptで以下のように呼び出します。

```html:sampleJS.html
<script>
  google.script.run.withSuccessHandler(function(serverResponse){
    console.log("success");
    console.log(serverResponse);
  }).withFailureHandler(function(serverResponse){
    console.log("failure");
    console.log(serverResponse);
  }).yourServerSideFunction();
</script>
```

```js:sample.gs
function yourServerSideFunction(){
  return 'response from server side';
}
```

クライアントサイドから`google.script.run`を用いて
サーバサイドの関数を呼び出す処理は非同期に実行されます。
従って`yourServerSideFunction()`の返り値を利用して何かしらの処理を行うには
返り値をコールバック関数の引数として渡してあげる必要があります。

通信成功時には`withSuccessHandler(callbackFunc)`,
失敗時には`withFailureHandler(callbackFunc)`が実行されます。
蛇足かもしれませんが実際に`yourServerSideFunction()`の返り値が渡されるのは
`callbackFunc(arg)`ですのでご注意を。

jQueryやSuperAgentを使ってAjax処理を書いたことのある方は馴染みある書き方かもしれませんね。
ちなみに`withSuccessHandler(callbackFunc)`と`withFailureHandler(callbackFunc)`は省略可能です。
より正確な情報は公式の[こちら](https://developers.google.com/apps-script/guides/html/reference/run)に書いてあります。

###2. クライアントサイド実装
それでは早速実装に移りましょう。

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta charset="utf-8">
  <?!= include('css'); ?>
  <!-- vue.js -->
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
  <div id="SS-table">
    <table>
      <tr>
        <th v-for="item in header">{{ item }}</th>
      </tr>
      <tr v-for="row in rows">
        <td v-for="item in row">{{ item }}</td>
      </tr>
    </table>
  </div>
  
  <?!= include('js'); ?>
</body>
</html>
```

```html:css.html
<style>
  table {
    border-collapse: collapse;
  }
  
  table th, table td {
    border: solid 2px gray;
  }

  table th{
    text-align: left;
    padding: 10px;
    color: #486666;
    background: #b5ffff;
  }

  table td{
    padding: 4px 10px;
    color: #486666;
  }
</style>
```

```html:js.html
<script> 
  var vm = new Vue({
    el: "#SS-table",
    data: {
      tableArray: [[]]
    },
    computed: {
      header: function(){
        return this.tableArray[0];
      },
      rows: function(){
        return this.tableArray.slice(1);
      }
    },
    methods: {
      initData: function(ssData){
        this.tableArray = ssData;
      }
    },
    created: function(){
      google.script.run
        .withSuccessHandler(this.initData)
        .withFailureHandler(function(arg){
          console.log(arg)
          alert("データの取得に失敗しました。");
        }).getSheetData();
    }
  });
</script>
```

`index.html`と`css.html`に関しては特に問題無いと思いますので、`js.html`を見ていきましょう。
先述の通り`google.script.run`は非同期で実行されるため、
返り値を`data`プロパティに初期値として設定することはできません。
そこで今回は`created`プロパティを用いて
サーバサイドからの返り値を`data`プロパティにセットしています。

`getSheetData()`の返り値が
`withSuccessHandler(callbackFunc)`の
コールバック関数である`initData(ssData)`の引数として渡され、
初期化が実行されるという流れですね。

これでサーバサイドからの返り値を使ってVueインスタンスを作成する仕組みが整いました。

###3. サーバサイド実装
サーバサイドの実装に移りましょう。
クライアントサイドではスプレッドシートの値を返すであろう関数`getSheetData()`が呼び出されていました。
こちらの関数を`server.gs`に追加します。

```js:server.gs
function doGet(request) {
  var template = 'index';
  return HtmlService.createTemplateFromFile(template).evaluate();
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename)
      .getContent();
}

function getSheetData(){
  var spreadSheetID = "yourSpreadsheetID";
  var sheetName = "シート1";
  var res = SpreadsheetApp.openById(spreadSheetID)
    .getSheetByName(sheetName).getDataRange().getValues();
  
  return res;
}
```
うん。GASを使ってスプレッドシートをゴニョゴニョしたことがある方ならば見慣れた処理だと思います。
よく分からないという方もスプレッドシートの内容を二次元配列にして返すという理解でOKです。
今回はこんな感じのスプレッドシートをサンプルに用意してみました。
![スクリーンショット 2018-04-19 22.11.35.png](https://qiita-image-store.s3.amazonaws.com/0/94505/7fa80769-1355-8bf8-58c9-225317defa70.png)

これで準備が整いました!!
今回新たにスプレッドシートへのスコープが必要になるので
アプリケーションを更新してスコープを追加しておきましょう。
[一番はじめの記事](https://qiita.com/kyhei_0727/items/f409ab53bac931d4eb77)でアプリケーションの公開を行ったときと同様の手順で更新ができます。

いざアクセスしてみると……
![スクリーンショット 2018-04-19 22.26.02.png](https://qiita-image-store.s3.amazonaws.com/0/94505/3dcc5c7a-1b90-c19d-10cb-b906f17aeb03.png)

**なんてこったす。**
ブラウザのコンソールでログを確認すると以下のようなメッセージが表示されています。
`Error: スクリプトが完了しましたが、返された値はサポートされている戻り値の型ではありませんでした。`
[公式のドキュメント](https://developers.google.com/apps-script/guides/html/reference/run)を見てみると以下のような記述が見つかります。
>Most types are legal, but not Date, Function, or DOM element besides form; see description

なるほど、どうやらスプレッドシートA列の日時列を`getValues()`した際に
`Date`オブジェクトとなりGASの制約に引っかかったみたいです。

サーバサイドの`getSheetData()`関数を修正し
`Date`オブジェクトが返り値に含まないようにしましょう。

```js:server.gs
function getSheetData(){
  var spreadSheetID = "yourSpreadsheetID";
  var sheetName = "シート1";
  var res = SpreadsheetApp.openById(spreadSheetID)
      .getSheetByName(sheetName).getDataRange().getValues();
  var formateDate = function(d){
    var addZero = function(n){
      return (n < 10)? "0" + String(n):String(n);
    }
    
    var year = String(d.getFullYear());
    var month = addZero(d.getMonth() + 1);
    var date = addZero(d.getDate());
    var hour = addZero(d.getHours());
    var minute = addZero(d.getMinutes());
    var second = addZero(d.getSeconds());
    return year + "/" + month + "/" + date + " " + hour + ":" + minute + ":" + second;
  };
  
  /* 有効な返り値に変換 */
  for(var i = 0; i < res.length; i ++){
    for(var j = 0; j < res[0].length; j ++){
      if(Object.prototype.toString.call(res[i][j]) == '[object Date]'){
        res[i][j] = formateDate(res[i][j]);
      }
    }
  }
  
  return res;
}
```

ちなみに別にここまでしなくとも…という方は、
`getValues()`の代わりに`getDisplayValues()`を使うと
強制的に全てのセルを`String`として取得できるので一撃で事が足ります。

さて、それでは満を持して再びエンドポイントへアクセスすると…
![スクリーンショット 2018-04-19 22.45.44.png](https://qiita-image-store.s3.amazonaws.com/0/94505/a931453c-c3af-1544-b0ea-79b14c7e23be.png)

お疲れ様でした！！！

###4. 終わりに
今回はスプレッドシートの内容をVueインスタンスを用いて描画してみました。
少し複雑なことをしようとすると、GAS特有の制約に意図せずハマったりするので気をつけたいところです…
それでもGASは公式のドキュメントが豊富でしっかりしている上に手軽に使えるので便利だなあと思います。

ここまでの３回の記事でGAS上での実装に必要な大体揃ってきたと思います。
次回はGoogle　Driveへのファイルアップローダの実装について書いていきます。